Compare commits

..
83 Commits
Author SHA1 Message Date
Jones fc9c1026b1 Release 2.1.8 (#385) 2024-09-22 13:21:19 +02:00
Jones aae49aeb63 Remove initial "unofficial branch" warning 2024-09-22 13:00:11 +02:00
Jones f7a6f04792 Make the update checker more accessible to users 2024-09-22 12:54:31 +02:00
Jones e36ba9840c Hide secure-chat toast from 1.20.5+ clients 2024-09-21 18:22:56 +02:00
Jones 40bceb71f1 Use the modern way of registering Velocity commands 2024-09-21 14:41:59 +02:00
Jones 2d3bf91444 Recode bandwidth statistic handling 2024-09-21 14:37:58 +02:00
Jones 45ab258a6f Fix upload-artifacts unable to find artifacts 2024-09-21 14:07:54 +02:00
Jones 600277660c General project base refactoring 2024-09-21 14:05:54 +02:00
MISHAandJones 6918afae52 JAR Downloads via GitHub Actions (#384)
* Update gradle.yml

* Update gradle.yml

* fix: smaller improvements to CI

* fix: wildcard pattern breaking CI

* fix(ci): revert wildcard patterns

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2024-09-21 13:45:15 +02:00
Jones 07cfc4ad1c Randomize X and Z positions in vehicle check 2024-09-19 17:30:06 +02:00
Jones 65eb9a503f Fix potential issue in the vehicle handler 2024-09-19 17:15:47 +02:00
Jones f5d7cc1511 Small collision check improvements 2024-09-19 16:59:25 +02:00
Jones 2cb0a48657 Add some more smaller movement validations 2024-09-19 16:45:39 +02:00
Jones 646f296039 Don't notify the channel about unhandled packets 2024-09-19 16:29:16 +02:00
Jones a698c26d39 Fix the 1.7.2-1.7.10 minimum Y coordinate issue 2024-09-19 16:21:19 +02:00
Jones 25136d6332 Small micro-optimizations in the packet registry 2024-09-19 13:11:20 +02:00
Jones ee4b1ffef4 Optimize processing of queued logins 2024-09-18 13:11:59 +02:00
Jones fe145836fe Use a better-suited data type for connected players 2024-09-18 12:34:34 +02:00
Jones 47100f4071 Bump Gradle from 8.10 to 8.10.1 2024-09-18 10:52:03 +02:00
Jones 57311996c8 Set Gradle groupId to xyz.jonesdev 2024-09-18 10:49:01 +02:00
Jones 7ad8e48431 Bump bungeecord-proxy from master-SNAPSHOT to 1.21-SNAPSHOT 2024-09-18 10:39:55 +02:00
Jones 154d999b88 Bump velocity-proxy from 3.3.0-SNAPSHOT to 3.4.0-SNAPSHOT 2024-09-18 10:39:04 +02:00
Jones bde637c9c5 Exempt Bedrock players from packet order checks 2024-09-18 10:07:56 +02:00
Jones cb1c62f478 Fix max total packets false positive on 1.7-1.8 2024-09-17 15:20:17 +02:00
Jones 3727742419 Add CaptchaGenerationStartEvent, CaptchaGenerationEndEvent 2024-09-17 14:52:15 +02:00
Jones f79bb38740 Remove a debug message 2024-09-17 14:48:47 +02:00
Jones 7d60ba85e7 Fix custom CAPTCHA background image not working 2024-09-17 14:44:31 +02:00
Jones cec7fa07c2 Fix another potential false positive with Geyser 2024-09-17 13:12:57 +02:00
Jones 2be2380e3f Fix invalid login mappings for 1.18-1.18.1 2024-09-16 14:18:08 +02:00
dependabot[bot] ae99ade03a Bump com.gradleup.shadow from 8.3.0 to 8.3.1 (#382)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  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-09-16 13:53:09 +02:00
Jones 59731a93f1 Release 2.1.7 (#381) 2024-09-15 14:09:43 +02:00
Jones ace55fbe7b Fix a small compilation error 2024-09-15 14:08:18 +02:00
Jones 696d3c5c81 Fix some Bedrock false positives 2024-09-15 14:03:00 +02:00
Jones a1e4ea3f5b Fix exception while serializing JsonArray (https://github.com/PaperMC/Velocity/pull/1426) 2024-09-14 15:45:19 +02:00
Jones 6b054bd56c Fix invalid block mappings for 1.14-1.14.3 2024-09-14 10:35:51 +02:00
Jones c8e2142dd9 Cap username length in LoginSuccess (clientbound) 2024-09-12 18:20:11 +02:00
Jones 1cf7ef6994 Add invalid window ID check to transactions 2024-09-11 21:14:08 +02:00
Jones af8aa103bf Randomize some more position-related values 2024-09-11 19:31:31 +02:00
Jones d1f2c15a8f Fix a false positive with the moved too far check 2024-09-11 19:28:29 +02:00
Jones 5a68c4d557 Added another vehicle Y position check 2024-09-11 19:23:58 +02:00
Jones 1438856531 Release 2.1.6 (#379) 2024-09-11 16:36:00 +02:00
AkkariinandJones 694725b798 Implement more interactionless checks (#378)
* Add minecart entity

* Add `VehicleMovePacket`

* Add `Animation` packet

* Add animation check.

* Fix the wrong entity minecart id for 1.21

* Add minecart for vehicle check.

* Exempt minecart check for 1.7-1.8 clients.

* Fix minecart id for lower than minecraft version 1.14.

* Fix wrong minecart entity id for 1.15

* Add missing license header

* Exempt animation check for 1.7.2 client due unknown disconnect error.

* style: small code style improvements

* style: even more style improvements

* refactor: inline some stuff

* style: small code cleanup

* fix: slot check not working in the first place

* style: don't use return; use an if-else block

* Fix animation check not working on 1.7.2+

* style: minor code style improvements

* fix: lombok compile error

* style: minor code style adjustments

* fix: optimize AnimationPacket

* fix: don't reassign unnecessary hand value

* style: code style fixes in *types

* style: add some comments to the *types

* [ci skip] style: small code style improvements

* Don't exempt 1.7-1.8 from the Minecart check

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2024-09-11 16:29:30 +02:00
Jones 47cb4d0840 Release 2.1.5 (#377) 2024-09-09 14:53:56 +02:00
dependabot[bot] 38ecb9ac06 Bump io.netty:netty-all from 4.1.112.Final to 4.1.113.Final (#376)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.112.Final to 4.1.113.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.112.Final...netty-4.1.113.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-09-09 14:31:43 +02:00
Jones 6657a3d674 Merge all custom logger wrappers into one 2024-09-08 18:32:35 +02:00
Jones 823ee21cea Clear up a small misconception in the code 2024-09-08 18:16:51 +02:00
Jones 4d206af0ae Don't allow players to send empty packets 2024-09-08 00:47:25 +02:00
Jones 2ff7b7efaa Fix gravity check false positive on 1.7 2024-09-07 23:19:10 +02:00
Jones a5efc1c796 Recode Y position and gravity calculation 2024-09-07 20:00:52 +02:00
Jones 85d5ce77b1 Implement SetPlayerRotationPacket packet order check 2024-09-07 19:34:46 +02:00
Jones 6c4d4b79d6 Implement PaddleBoatPacket packet order check 2024-09-07 19:34:19 +02:00
Jones 364b1c5b31 Optimize image RGB buffer writing 2024-09-07 14:48:55 +02:00
Jones 4edf04ede5 Randomize some placeholder variables during login 2024-09-07 12:53:24 +02:00
Jones 01da1a70ea Remove useless PlayerInfoPacket 2024-09-07 12:00:53 +02:00
Jones 9bb7c7cd60 Only log expired database entries if there are any 2024-09-07 11:20:12 +02:00
Jones aaa7588dcd Don't load the database driver twice 2024-09-07 11:19:06 +02:00
Jones 8d378d56ef Use regular JdbcConnectionSource, not pooled 2024-09-07 11:17:49 +02:00
Jones 476b1704b8 VarInt encoding optimizations (https://github.com/PaperMC/Velocity/commit/f034c0277d60e9f0a80e484832aec831cbc298f7) 2024-09-06 23:16:41 +02:00
Jones 26c31ceeaf VarInt reading optimizations (https://github.com/PaperMC/Velocity/pull/1418) 2024-09-06 23:07:26 +02:00
Jones 40f65ef5eb Rename 1.16(.2) codecs for more clarity 2024-09-06 22:54:50 +02:00
Jones 7736ccff86 Fix multiple block types being incorrect on 1.16 2024-09-06 22:53:24 +02:00
Jones 437a547ec3 [ci skip] Fix translation errors in copyright notice 2024-09-06 22:42:03 +02:00
Jones 4ec65218da [ci skip] Remove unused configuration entries 2024-09-06 22:31:15 +02:00
Jones de45e9f808 Mention TransferTool for Geyser in the config 2024-09-04 17:38:02 +02:00
Jones 87e768da0e Release 2.1.4 (#375) 2024-09-03 16:29:22 +02:00
Jones daa773bff1 General code style improvements and API fixes 2024-08-31 19:11:55 +02:00
Jones 1e43835d0b Quieten decoder exceptions to improve performance (#373)
* fix: use QuietDecoderException unless debug is enabled

* fix(bukkit): allow for more debug in the decoder
2024-08-31 13:57:41 +02:00
Jones efb8b3cd39 Don't handle the handshake for server pings 2024-08-31 13:49:32 +02:00
Jones 540899e92c Fix UserBlacklistedEvent not being called 2024-08-31 13:22:39 +02:00
Jones ef6140d47f Fix wrong usage for getting the blacklist score 2024-08-30 22:12:29 +02:00
Jones 27ca5a8368 Merge failCountCache and blacklist into one 2024-08-30 22:11:34 +02:00
Jones 1a43242c24 Clean up some inconsistent code style 2024-08-30 21:17:26 +02:00
Jones 65eeec6e17 Optimize login check order/priority 2024-08-30 21:17:14 +02:00
Jones d8e4dbd6cf Use host address instead of raw InetAddress 2024-08-30 21:12:03 +02:00
Jones 32973e674f Move ratelimiter implementations to the common module 2024-08-30 20:44:14 +02:00
Jones 05fa6de119 Fix some Java naming convention violations 2024-08-30 13:15:16 +02:00
Jones cf0f4e0b96 Code style improvements and micro-optimizations 2024-08-30 12:47:34 +02:00
Jones 0ae3c6c67b Remove unnecessary ctx.channel() calls 2024-08-30 12:37:25 +02:00
Jones aae564d16c Fix missing parentheses potentially causing problems 2024-08-29 18:38:16 +02:00
Jones f6338ba844 Recode the join rate-limiter 2024-08-29 18:29:48 +02:00
Jones d892645779 Reduce usage of channel.isActive() 2024-08-29 18:14:09 +02:00
Jones def40be5c0 Release 2.1.3 (#369) 2024-08-28 15:33:53 +02:00
Jones d4bdf86fcb Fix NullPointerException 2024-08-28 15:28:10 +02:00
230 changed files with 3233 additions and 3080 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[{*.java,*.kt}]
[*.java]
ij_java_wrap_long_lines = true
[{*.md,*.yml}]
+32 -3
View File
@@ -9,9 +9,9 @@ name: Sonar CI
on:
push:
branches: [ "main", "dev" ]
branches: [ "main" ]
pull_request:
branches: [ "main", "dev" ]
branches: [ "main" ]
permissions:
contents: read
@@ -22,12 +22,41 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
uses: gradle/gradle-build-action@v3
with:
arguments: build-sonar
- name: Upload Bukkit Artifact
uses: actions/upload-artifact@v3.1.3
with:
# Artifact name
name: Sonar
# A file, directory or wildcard pattern that describes what to upload
path: bukkit/build/libs/Sonar-Bukkit.jar
if-no-files-found: error
- name: Upload BungeeCord Artifact
uses: actions/upload-artifact@v3.1.3
with:
# Artifact name
name: Sonar
# A file, directory or wildcard pattern that describes what to upload
path: bungeecord/build/libs/Sonar-Bungee.jar
if-no-files-found: error
- name: Upload Velocity Artifact
uses: actions/upload-artifact@v3.1.3
with:
# Artifact name
name: Sonar
# A file, directory or wildcard pattern that describes what to upload
path: velocity/build/libs/Sonar-Velocity.jar
if-no-files-found: error
@@ -9,5 +9,11 @@ dependencies {
compileOnly(rootProject.libs.annotations)
}
tasks {
shadowJar {
archiveFileName = "sonar-api-${rootProject.version}.jar"
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -21,17 +21,17 @@ 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;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.text.DecimalFormat;
import java.util.UUID;
@@ -41,7 +41,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
* we have to use their platform module to support BungeeCord and Bukkit.
*/
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@@ -49,14 +49,8 @@ public interface Sonar {
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* @return The platform the plugin is being run on
*/
@NotNull SonarPlatform getPlatform();
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
*/
@NotNull LoggerWrapper getLogger();
@NotNull SonarConfiguration getConfig();
@@ -65,29 +59,20 @@ public interface Sonar {
VerifiedPlayerController getVerifiedPlayerController();
@NotNull Verbose getVerboseHandler();
@NotNull ActionBarNotificationHandler getActionBarNotificationHandler();
@NotNull Notification getNotificationHandler();
@NotNull ChatNotificationHandler getChatNotificationHandler();
@NotNull SystemTimer getLaunchTimer();
@NotNull SonarStatistics getStatistics();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
void setActionBarNotificationHandler(final @NotNull ActionBarNotificationHandler notificationHandler);
/**
* Set a custom notification handler
*/
@SuppressWarnings("unused")
void setNotificationHandler(final @NotNull Notification notificationHandler);
void setChatNotificationHandler(final @NotNull ChatNotificationHandler notificationHandler);
/**
* Reloads the entire plugin
*/
void reload();
@NotNull
@@ -97,19 +82,16 @@ public interface Sonar {
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default AttackTracker getAttackTracker() {
// We don't want anyone else to create a new instance.
return AttackTracker.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -26,21 +26,15 @@ import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
BUKKIT("Bukkit", 19110, "packet_handler",
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"),
BUNGEE("BungeeCord", 19109, "inbound-boss",
pipeline -> "packet-encoder"),
VELOCITY("Velocity", 19107, "handler",
pipeline -> "minecraft-encoder");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
private final String connectionHandler;
private final Function<ChannelPipeline, String> encoder;
}
@@ -19,21 +19,20 @@ package xyz.jonesdev.sonar.api;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import xyz.jonesdev.sonar.api.exception.AlreadyInitializedException;
@UtilityClass
public class SonarSupplier {
private Sonar sonar;
private Sonar _sonar;
public void set(final @NotNull Sonar s) {
if (sonar != null) {
throw new AlreadyInitializedException();
public void set(final @NotNull Sonar instance) {
if (_sonar != null) {
throw new AlreadyInitializedException("Sonar is already initialized");
}
sonar = Objects.requireNonNull(s);
_sonar = instance;
}
public @NotNull Sonar get() {
return sonar;
return _sonar;
}
}
@@ -42,7 +42,6 @@ public final class SonarVersion {
this.formatted = version + " (" + gitCommit + ")";
}
// Taken from
// https://github.com/PaperMC/Velocity/pull/1336/
private static @NotNull Manifest getManifest() {
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
@@ -21,7 +21,6 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@@ -34,23 +33,12 @@ public final class InvocationSource {
private final Predicate<String> permissionFunction;
/**
* @return True, if {@link InvocationSource#uuid} is not null
* @apiNote This indicates a player as a player will always have a UUID
* @return true if {@link InvocationSource#uuid} is not null
*/
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 void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
@@ -26,6 +26,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.update.UpdateChecker;
import java.util.*;
@@ -66,11 +67,18 @@ public interface SonarCommand {
// Invoke subcommand
command.invoke(source, args);
}, () -> {
final UpdateChecker.CheckResult checkResult = UpdateChecker.getLastCheckResult();
Component version = Component.text(Sonar.get().getVersion().getFormatted());
if (checkResult.getConfigKey() != null) {
version = version.append(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main." + checkResult.getConfigKey())));
}
// Print standard help message
source.sendMessage(MiniMessage.miniMessage().deserialize(
String.join("<newline>",
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
Placeholder.component("version", version),
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
@@ -94,7 +102,7 @@ public interface SonarCommand {
});
}
static void prepareCachedMessages() {
static void prepareCachedTabSuggestions() {
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
@@ -25,34 +25,15 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SubcommandInfo {
/**
* @return Name of the subcommand
*/
String name();
/**
* @return Aliases of the subcommand
*/
String[] aliases() default {};
/**
* @return Arguments of the subcommand
*/
String[] arguments() default {};
/**
* @return Whether the subcommands requires arguments
*/
boolean argumentsRequired() default true;
/**
* @return Whether the subcommands can only be executed by a player
*/
boolean onlyPlayers() default false;
/**
* @return Whether the subcommands can only be executed by console
*/
boolean onlyConsole() default false;
}
@@ -21,6 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.MemorySection;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.InputStream;
@@ -31,8 +32,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
@Getter
public final class SimpleYamlConfig {
private final File file;
@@ -90,7 +89,7 @@ public final class SimpleYamlConfig {
public int getInt(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return 0;
}
return yaml.getInt(path);
@@ -98,7 +97,7 @@ public final class SimpleYamlConfig {
public boolean getBoolean(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return false;
}
return yaml.getBoolean(path);
@@ -106,7 +105,7 @@ public final class SimpleYamlConfig {
public @NotNull String getString(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return "";
}
final Object object = yaml.get(path);
@@ -118,7 +117,7 @@ public final class SimpleYamlConfig {
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getStringList(path);
@@ -126,7 +125,7 @@ public final class SimpleYamlConfig {
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getIntegerList(path);
@@ -27,10 +27,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -52,24 +51,6 @@ public final class SonarConfiguration {
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[config] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[config] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[config] " + message, args);
}
};
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
@@ -85,14 +66,14 @@ public final class SonarConfiguration {
final URL defaultLanguageFile = Sonar.class.getResource("/assets/language.properties");
// Make sure the file actually exists before trying to copy it
if (defaultLanguageFile == null) {
LOGGER.error("Cannot check for custom language (is the file missing?)");
Sonar.get().getLogger().error("Cannot check for custom language (is the file missing?)");
return DEFAULT_FALLBACK_LANGUAGE;
}
// Copy the file to the plugin data directory
try (final InputStream inputStream = defaultLanguageFile.openStream()) {
Files.copy(inputStream, languageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
LOGGER.error("Error copying file: {}", exception);
Sonar.get().getLogger().error("Error copying file: {}", exception);
return DEFAULT_FALLBACK_LANGUAGE;
}
}
@@ -106,13 +87,13 @@ public final class SonarConfiguration {
// Try parsing the property as a language
return Language.fromCode(property);
} catch (Throwable throwable) {
LOGGER.error("Could not find requested language: {}", throwable);
LOGGER.error("You can view a full list of valid language codes here:");
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
Sonar.get().getLogger().error("Could not find requested language: {}", throwable);
Sonar.get().getLogger().error("You can view a full list of valid language codes here:");
Sonar.get().getLogger().error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
Sonar.get().getLogger().error("If a translation does not exist yet, Sonar will use English (en).");
}
} catch (IOException exception) {
LOGGER.error("Error reading language file: {}", exception);
Sonar.get().getLogger().error("Error reading language file: {}", exception);
}
return DEFAULT_FALLBACK_LANGUAGE;
}
@@ -131,13 +112,13 @@ public final class SonarConfiguration {
final String property = System.getProperty("user.language", "en");
language = Language.fromCode(property);
// Make sure the user knows that we're using the system language for translations
LOGGER.info("Using system language ({}) for translations.", language);
Sonar.get().getLogger().info("Using system language ({}) for translations.", language);
} catch (Exception exception) {
LOGGER.warn("Could not use system language for translations.");
LOGGER.warn("Using default language ({}) for translations.", language);
Sonar.get().getLogger().warn("Could not use system language for translations.");
Sonar.get().getLogger().warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", language);
Sonar.get().getLogger().info("Using custom language ({}) for translations.", language);
}
// Load all configurations
@@ -177,12 +158,12 @@ public final class SonarConfiguration {
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
// Warn the user if the verification timing is set to NEVER
if (verification.timing == Verification.Timing.NEVER) {
LOGGER.warn(" ");
LOGGER.warn("You have set the verification timing to 'NEVER'.");
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
LOGGER.warn("Please only edit this option if you really know what you are doing.");
LOGGER.warn(" ");
Sonar.get().getLogger().warn(" ");
Sonar.get().getLogger().warn("You have set the verification timing to 'NEVER'.");
Sonar.get().getLogger().warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
Sonar.get().getLogger().warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
Sonar.get().getLogger().warn("Please only edit this option if you really know what you are doing.");
Sonar.get().getLogger().warn(" ");
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
@@ -202,10 +183,11 @@ public final class SonarConfiguration {
final String backgroundPath = generalConfig.getString("verification.checks.map-captcha.background");
if (!backgroundPath.isEmpty()) {
try (final InputStream inputStream = new FileInputStream(backgroundPath)) {
verification.map.backgroundImage = inputStream;
} catch (IOException exception) {
Sonar.get().getLogger().error("Could not find background image", exception);
final File backgroundFile = new File(pluginFolder, backgroundPath);
if (backgroundFile.exists()) {
verification.map.backgroundImage = backgroundFile;
} else {
Sonar.get().getLogger().error("Could not find background image {}", backgroundFile.getAbsolutePath());
}
}
@@ -216,7 +198,6 @@ public final class SonarConfiguration {
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
@@ -264,7 +245,7 @@ public final class SonarConfiguration {
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
Placeholder.component("prefix", prefix));
SonarCommand.prepareCachedMessages();
SonarCommand.prepareCachedTabSuggestions();
supportUrl = messagesConfig.getString("support-url");
header = MiniMessage.miniMessage().deserialize(
@@ -303,7 +284,7 @@ public final class SonarConfiguration {
final String resourceName = url + "/" + language.getCode() + ".yml";
URL result = Sonar.class.getResource("/assets/" + resourceName);
if (result == null) {
LOGGER.warn("Could not find " + resourceName + "! Using en.yml!");
Sonar.get().getLogger().warn("Could not find " + resourceName + "! Using en.yml!");
result = Objects.requireNonNull(Sonar.class.getResource("/assets/" + url + "/en.yml"));
}
return result;
@@ -317,8 +298,8 @@ public final class SonarConfiguration {
return output;
}
public String formatAddress(final InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
public @NotNull String formatAddress(final @NotNull InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.getHostAddress() : "<ip address withheld>";
}
@Getter
@@ -386,7 +367,7 @@ public final class SonarConfiguration {
private int maxDuration;
private int maxTries;
private String alphabet;
private InputStream backgroundImage;
private File backgroundImage;
}
@Getter
@@ -434,7 +415,6 @@ public final class SonarConfiguration {
private Pattern validNameRegex;
private Pattern validLocaleRegex;
private int maxLoginPackets;
private int readTimeout;
private int writeTimeout;
private int reconnectDelay;
@@ -489,7 +469,7 @@ public final class SonarConfiguration {
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
private boolean loaded;
}
private Type type;
@@ -15,12 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.controller;
package xyz.jonesdev.sonar.api.database.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -28,44 +28,27 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import java.io.File;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[database] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[database] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[database] " + message, args);
}
};
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
@Getter
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@@ -74,26 +57,21 @@ public final class VerifiedPlayerController {
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
LOGGER.warn("Configure a database to save verified players.");
Sonar.get().getLogger().warn("Configure a database to save verified players.");
return;
}
// Make sure to only download the driver once
if (!cachedDatabaseType.isDownloaded()) {
LOGGER.info("Downloading {} driver version {}",
// Make sure to only load the driver once per database type
if (!cachedDatabaseType.isLoaded()) {
Sonar.get().getLogger().info("Loading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
// Download and load the driver for the current database
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
cachedDatabaseType.setLoaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
final String jdbcURL;
@@ -112,7 +90,7 @@ public final class VerifiedPlayerController {
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
connectionSource = new JdbcConnectionSource(jdbcURL,
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
@@ -120,10 +98,12 @@ public final class VerifiedPlayerController {
// Create database table
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException exception) {
// Duplicate index
// I know this isn't the best method of handling it,
// but I don't know how else I could address this issue.
} catch (SQLException ignored) {
/*
* This is caused by a duplicate index;
* I know this isn't the best method of handling it,
* but I don't know how else I could address this issue.
*/
}
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
@@ -136,13 +116,13 @@ public final class VerifiedPlayerController {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
dao.queryForAll().forEach(verifiedPlayer -> cache.add(verifiedPlayer.getFingerprint()));
} catch (SQLException exception) {
LOGGER.error("Error initializing database: {}", exception);
exception.printStackTrace(System.err);
}
});
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
exception.printStackTrace(System.err);
}
}
@@ -155,7 +135,7 @@ public final class VerifiedPlayerController {
try {
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
exception.printStackTrace(System.err);
}
}
}
@@ -172,11 +152,11 @@ public final class VerifiedPlayerController {
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
if (oldEntries != null && !oldEntries.isEmpty()) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -185,10 +165,10 @@ public final class VerifiedPlayerController {
* First, remove the player from the local cache and then,
* secondly, asynchronously add the player to the database.
*
* @param inetAddress InetAddress of the VerifiedPlayer model
* @param fingerprint Fingerprint of the verified player
*/
public void remove(final @NotNull String inetAddress) {
_remove(inetAddress);
public void remove(final @NotNull String fingerprint) {
cache.remove(fingerprint);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -204,7 +184,7 @@ public final class VerifiedPlayerController {
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
.eq("ip_address", inetAddress)
.eq("fingerprint", fingerprint)
.query();
if (verifiedPlayer != null) {
@@ -213,20 +193,11 @@ public final class VerifiedPlayerController {
}
}
} catch (SQLException exception) {
LOGGER.error("Error trying to remove entry: {}", exception);
exception.printStackTrace(System.err);
}
});
}
/**
* Locally remove the object from the cache
*
* @param inetAddress IP address of the player
*/
private void _remove(final @NotNull String inetAddress) {
cache.remove(inetAddress);
}
/**
* First, cache the player locally and then,
* secondly, asynchronously add the player to the database.
@@ -234,7 +205,7 @@ public final class VerifiedPlayerController {
* @param player VerifiedPlayer model
*/
public void add(final @NotNull VerifiedPlayer player) {
_add(player);
cache.add(player.getFingerprint());
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -251,21 +222,11 @@ public final class VerifiedPlayerController {
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
LOGGER.error("Error trying to add entry: {}", exception);
exception.printStackTrace(System.err);
}
});
}
/**
* Locally cache the object
*
* @param player VerifiedPlayer model
*/
private void _add(final @NotNull VerifiedPlayer player) {
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUuid());
}
/**
* Returns the number of verified IP addresses
*
@@ -275,26 +236,6 @@ public final class VerifiedPlayerController {
return cache.size();
}
/**
* Returns the sum of verified IP addresses
* and their respective collection of UUIDs
*
* @return Exact size of all total verified players
*/
@Deprecated
public synchronized int exactSize() {
return cache.values().stream()
.mapToInt(Collection::size)
.sum();
}
/**
* @return {@link java.util.Collection} of UUIDs associated with an IP address
*/
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
@@ -307,29 +248,8 @@ public final class VerifiedPlayerController {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
exception.printStackTrace(System.err);
}
}
}
/**
* @param inetAddress IP address
* @param uuid UUID associated to the IP
* @return Whether the local cache contains the IP and UUID
*/
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
final Collection<UUID> got = cache.get(inetAddress);
if (got != null) {
return got.contains(uuid);
}
return false;
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
*/
public boolean has(final @NotNull String inetAddress) {
return cache.containsKey(inetAddress);
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.model;
package xyz.jonesdev.sonar.api.database.model;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@@ -25,34 +25,23 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
import java.sql.Timestamp;
import java.util.UUID;
@Getter
@ToString
@DatabaseTable(tableName = "sonar_fingerprints")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DatabaseTable(tableName = "sonar_verified_players")
public final class VerifiedPlayer {
@SuppressWarnings("unused")
@DatabaseField(generatedId = true)
private int id;
@DatabaseField(
columnName = "ip_address",
columnName = "fingerprint",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 16
width = 48
)
private String inetAddress;
@DatabaseField(
columnName = "player_uuid",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 36
)
private UUID playerUuid;
private String fingerprint;
@DatabaseField(
columnName = "timestamp",
@@ -60,17 +49,9 @@ public final class VerifiedPlayer {
)
private Timestamp timestamp;
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
final @NotNull UUID playerUuid,
public VerifiedPlayer(final @NotNull String fingerprint,
final long timestamp) {
this(inetAddress.toString(), playerUuid, timestamp);
}
public VerifiedPlayer(final @NotNull String inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this.inetAddress = inetAddress;
this.playerUuid = playerUuid;
this.fingerprint = fingerprint;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -17,13 +17,13 @@
package xyz.jonesdev.sonar.api.event;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface SonarEventListener {
/**
* Handles the publishing of an event
*
* @param event [Cancellable] Sonar event
* Handles the publishing of an event through Sonar's API
*/
void handle(final SonarEvent event);
void handle(final @NotNull SonarEvent event);
}
@@ -35,12 +35,9 @@ public final class SonarEventManager {
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
// Don't post a task if there are no listeners
// Don't post an event if there are no listeners
if (EVENT_LISTENERS.isEmpty()) {
return;
}
@@ -61,11 +58,13 @@ public final class SonarEventManager {
* Registers one (or more) event listeners
*
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
* @see #unregisterListener(SonarEventListener[]) Unregister an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
public void registerListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
}
/**
@@ -73,10 +72,12 @@ public final class SonarEventManager {
*
* @param listeners One (or more) listeners to unregister
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
* @see #registerListener(SonarEventListener[]) Register an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
public void unregisterListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
}
}
@@ -20,8 +20,8 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
@Getter
@ToString
@@ -0,0 +1,32 @@
/*
* Copyright (C) 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.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@Getter
@ToString
@RequiredArgsConstructor
public final class CaptchaGenerationEndEvent implements SonarEvent {
private final SystemTimer timer;
private final int amountGenerated;
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 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.api.event.impl;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
@Getter
@Setter
@AllArgsConstructor
public final class CaptchaGenerationStartEvent implements SonarEvent {
private CaptchaGenerator captchaGenerator;
}
@@ -22,15 +22,12 @@ import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.UUID;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@Getter
@ToString
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser user;
private final long timeTakenToVerify;
private final SystemTimer loginTimer;
}
@@ -15,7 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api;
package xyz.jonesdev.sonar.api.exception;
final class AlreadyInitializedException extends RuntimeException {
import org.jetbrains.annotations.NotNull;
public final class AlreadyInitializedException extends RuntimeException {
public AlreadyInitializedException(final @NotNull String message) {
super(message);
}
public Throwable fillInStackTrace() {
return this;
}
}
@@ -15,7 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api;
package xyz.jonesdev.sonar.api.exception;
import xyz.jonesdev.sonar.api.Sonar;
public final class ReflectiveOperationException extends RuntimeException {
public ReflectiveOperationException(final Throwable throwable) {
@@ -22,11 +22,10 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;
@@ -37,38 +36,17 @@ import java.util.concurrent.ConcurrentMap;
public final class Fallback {
public static final Fallback INSTANCE = new Fallback();
// Map of all players connected to the server in general
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
// Map of all connected IP addresses (used for fast checking)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
private final ConcurrentMap<InetAddress, Boolean> connected = new ConcurrentHashMap<>(256);
private final FallbackLoginQueue queue = new FallbackLoginQueue();
@Setter
private Cache<InetAddress, Byte> blacklist;
private Cache<String, Integer> blacklist;
@Setter
private long blacklistTime;
@Setter
private CaptchaGenerator captchaGenerator;
private final @NotNull FallbackQueue queue = new FallbackQueue();
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
private final LoggerWrapper logger = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[fallback] " + message, args);
}
};
@Setter
private Ratelimiter<InetAddress> ratelimiter;
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
@@ -23,8 +23,6 @@ import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.Sonar;
import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
@@ -32,27 +30,24 @@ import java.util.concurrent.Executors;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackQueue {
public final class FallbackLoginQueue {
// Fixed thread pool executor for all new verifications
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
128, 0.75f, Runtime.getRuntime().availableProcessors());
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(512);
public void poll() {
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// Iterate through the map and process up to maxQueuePolls entries
final int maxQueuePolls = Math.min(players.size(), Sonar.get().getConfig().getQueue().getMaxQueuePolls());
// No need to initiate an executor service task if nobody is currently queued
if (maxQueuePolls <= 0) return;
// We need to be cautious here since we don't want any concurrency issues
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = players.entrySet().iterator();
while (iterator.hasNext() && index++ < maxQueuePolls) {
// Run the cached runnable
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
QUEUE_EXECUTOR.execute(entry.getValue());
// Remove runnable from the map
iterator.remove();
}
QUEUE_EXECUTOR.execute(() -> {
final var iterator = players.entrySet().iterator();
for (int index = 0; index < maxQueuePolls && iterator.hasNext(); index++) {
iterator.next().getValue().run();
iterator.remove();
}
});
}
}
@@ -26,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.UUID;
public interface FallbackUser {
@NotNull
@@ -45,15 +44,14 @@ public interface FallbackUser {
SystemTimer getLoginTimer();
@NotNull
UUID getOfflineUuid();
String getFingerprint();
boolean isReceivedClientSettings();
@NotNull
String getUsername();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isForceCaptcha();
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
void setForceCaptcha(final boolean isForceCaptcha);
boolean isGeyser();
@@ -65,14 +63,6 @@ public interface FallbackUser {
*/
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
*
@@ -97,16 +87,4 @@ public interface FallbackUser {
ReferenceCountUtil.release(msg);
}
}
/**
* Disconnects the player who failed the verification
* and caches them in FAILED_VERIFICATIONS.
* If the player fails the verification twice,
* 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 @NotNull String reason);
}
@@ -17,9 +17,15 @@
package xyz.jonesdev.sonar.api.fallback.captcha;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
@FunctionalInterface
public interface CaptchaGenerator {
BufferedImage createImage(final char[] answer);
/**
* Generates a {@link java.awt.image.BufferedImage} that shows the answer to the CAPTCHA
*/
@NotNull BufferedImage createImage(final char @NotNull [] answer);
}
@@ -15,12 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command.subcommand.argument;
package xyz.jonesdev.sonar.api.fallback.ratelimit;
public @interface Argument {
import org.jetbrains.annotations.NotNull;
/**
* @return Returns the name of the argument
*/
String value();
@FunctionalInterface
public interface Ratelimiter<T> {
boolean attempt(final @NotNull T t);
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 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.api.fingerprint;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@UtilityClass
public class FingerprintingUtil {
/**
* Returns a hex string representing a hash of the username and IP address
*/
public @NotNull String getFingerprint(final @NotNull String username,
final @NotNull String hostAddress) {
final int hash0 = username.hashCode();
final int hash1 = hostAddress.hashCode();
final int combined = hash0 + hash1;
return Integer.toHexString(hash0 >> 4)
+ Integer.toHexString(combined << 2)
+ Integer.toHexString(hash1 >> 4);
}
}
@@ -33,7 +33,6 @@ public interface LoggerWrapper {
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -23,21 +23,21 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public final class Verbose extends Observable {
public final class ActionBarNotificationHandler extends NotificationHandler {
private int animationIndex;
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
@@ -63,10 +63,10 @@ public final class Verbose extends Observable {
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getPerSecondIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalIncomingBandwidth())),
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalOutgoingBandwidth())),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -29,13 +29,13 @@ import java.util.List;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
public final class Notification extends Observable {
public final class ChatNotificationHandler extends NotificationHandler {
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
@@ -63,10 +63,10 @@ public final class Notification extends Observable {
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getPerSecondIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getPerSecondOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalIncomingBandwidth())),
Placeholder.unparsed("outgoing-traffic-ttl", formatMemory(Sonar.get().getStatistics().getTotalOutgoingBandwidth())),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
@@ -74,7 +74,7 @@ public final class Notification extends Observable {
}
// Send the title and chat messages to all online players
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
for (final UUID subscriber : Sonar.get().getChatNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@@ -25,10 +25,10 @@ import java.util.UUID;
import java.util.Vector;
@Getter
public abstract class Observable {
public abstract class NotificationHandler {
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public abstract void observe();
public abstract void handleNotification();
/**
* @param uuid UUID of the player
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.jvm;
package xyz.jonesdev.sonar.api.profiler;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
@@ -24,7 +24,7 @@ import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
public class SimpleProcessProfiler {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
@@ -0,0 +1,53 @@
/*
* Copyright (C) 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.api.statistics;
@SuppressWarnings("unused")
public interface SonarStatistics {
long getConnectionsPerSecond();
long getLoginsPerSecond();
long getCurrentIncomingBandwidth();
long getCurrentOutgoingBandwidth();
long getTotalIncomingBandwidth();
long getTotalOutgoingBandwidth();
String getPerSecondIncomingBandwidthFormatted();
String getPerSecondOutgoingBandwidthFormatted();
int getTotalPlayersJoined();
int getTotalPlayersVerified();
int getTotalSuccessfulVerifications();
int getTotalFailedVerifications();
long getCurrentAttemptedVerifications();
int getTotalAttemptedVerifications();
long getCurrentBlacklistSize();
long getTotalBlacklistSize();
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.attack;
package xyz.jonesdev.sonar.api.tracker;
import lombok.*;
import org.jetbrains.annotations.Nullable;
@@ -27,12 +27,13 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@@ -68,7 +69,7 @@ public final class AttackTracker {
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().observe();
Sonar.get().getChatNotificationHandler().handleNotification();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
@@ -143,11 +144,9 @@ public final class AttackTracker {
// Reset the attack status
currentAttack = null;
// Reset threshold
attackThreshold = 0;
}
} else {
// Reset threshold
attackThreshold = 0;
}
}
@@ -15,14 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.update;
package xyz.jonesdev.sonar.api.update;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
@@ -36,24 +37,8 @@ import java.util.concurrent.Executors;
@UtilityClass
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
@Getter
private CheckResult lastCheckResult = CheckResult.UNRESOLVED;
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
@@ -66,17 +51,21 @@ public class UpdateChecker {
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getVersion());
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
Sonar.get().getLogger().warn("A new version of Sonar is available: {}", latestStableRelease);
Sonar.get().getLogger().warn("Please make sure to update to the latest version to ensure stability and security:");
Sonar.get().getLogger().warn("https://github.com/jonesdevelopment/sonar/releases/latest");
lastCheckResult = CheckResult.OUTDATED_VERSION;
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().getGitBranch().equals("main")) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.info("You are currently using the latest stable release of Sonar!");
Sonar.get().getLogger().warn("You are currently using an unreleased version of Sonar!");
Sonar.get().getLogger().warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
lastCheckResult = CheckResult.UNSTABLE_VERSION;
} else if (lastCheckResult != CheckResult.LATEST_VERSION) {
Sonar.get().getLogger().info("You are currently using the latest stable release of Sonar!");
lastCheckResult = CheckResult.LATEST_VERSION;
}
} catch (Throwable throwable) {
LOGGER.warn("Unable to retrieve version information: {}", throwable);
Sonar.get().getLogger().warn("Unable to retrieve version information: {}", throwable);
lastCheckResult = CheckResult.API_ERROR;
}
});
}
@@ -118,4 +107,16 @@ public class UpdateChecker {
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
@Getter
@RequiredArgsConstructor
public enum CheckResult {
API_ERROR("api-error"),
OUTDATED_VERSION("outdated-version"),
UNSTABLE_VERSION("unstable-version"),
LATEST_VERSION(null),
UNRESOLVED(null);
private final String configKey;
}
}
+1 -1
View File
@@ -86,7 +86,7 @@ allprojects {
tasks {
// This is a small wrapper tasks to simplify the building process
register("build-sonar") {
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
val subprojects = listOf("api", "captcha", "common", "bukkit", "bungeecord", "velocity")
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
dependsOn(buildTasks)
}
File renamed without changes.
@@ -120,8 +120,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -45,7 +45,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
return true;
}
@Override
@@ -1,3 +1,20 @@
/*
* Copyright (C) 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.fallback;
import io.netty.buffer.ByteBufAllocator;
@@ -7,6 +24,7 @@ import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
import java.util.HashSet;
@@ -16,7 +34,7 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
@@ -28,9 +46,9 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
public void add(final @NotNull ChannelHandler channelHandler) {
if (channelHandler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) channelHandler);
}
}
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
@@ -28,14 +28,15 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
@@ -44,7 +45,7 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
final ChannelInactiveListener inactiveListener = pipeline.get(ChannelInactiveListener.class);
if (inactiveListener != null) {
inactiveListener.add(handler);
@@ -66,8 +67,11 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
/*
* Reset the reader index, so we can read the content;
* We also have to make sure to store the original reader index.
* We do this to 'spoof' the packet state - we want to read it without the server knowing.
*/
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
@@ -82,7 +86,6 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -91,27 +94,27 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
throw DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
throw DEBUG ? new DecoderException("Could not read packet to end") : QuietDecoderException.INSTANCE;
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
/*
* Useful resources:
* https://wiki.vg/Protocol#Handshaking
* https://wiki.vg/Protocol#Login
*/
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
@@ -121,23 +124,22 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
throw DEBUG ? new DecoderException("Bad intent " + handshake.getIntent()) : QuietDecoderException.INSTANCE;
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
final ChannelHandler inboundHandler = ctx.pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -25,10 +25,8 @@ import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
@@ -36,7 +34,9 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.bukkit.SonarBukkit.INITIALIZE_LISTENER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
@@ -196,8 +196,7 @@ public class FallbackBukkitInjector {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
// Check if the field has the correct generic type; We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
@@ -235,27 +234,29 @@ public class FallbackBukkitInjector {
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
/*
* Inject our own channel initializer into the original field
* If the plugin is not fully enabled but executes the injection,
* put an initializer to close the new connection automatically.
*/
if (!INITIALIZE_LISTENER.isDone()) {
childHandlerField.set(bootstrap, new ChannelInitializer<>() {
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
protected void initChannel(final @NotNull Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
});
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
final ChannelHandler _bootstrap = bootstrap;
INITIALIZE_LISTENER.thenAccept(__ -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
childHandlerField.set(_bootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
pipeline.addFirst(FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
@@ -34,7 +34,6 @@ import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle _handleMethod;
@@ -84,7 +83,7 @@ public final class BukkitJoinListener implements Listener {
}
// Make sure the player is not a fake player
if (FakeChannelUtil.isFakePlayer(channel)) {
if (FakeChannelUtil.isFakeChannel(channel)) {
return;
}
@@ -26,6 +26,8 @@ dependencies {
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
archiveFileName = "Sonar-Bungee.jar" // Hardcode a shortened version of the name
}
}
@@ -108,8 +108,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -26,13 +26,15 @@ import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.STATUS;
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
@@ -48,20 +50,20 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
// Skip any unknown or invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
if (handshake.getRequestedProtocol() == STATUS) {
ctx.pipeline().remove(this);
} else {
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
}
}
// Intercept any server login packet by the client
@@ -77,7 +79,7 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
@@ -86,7 +88,6 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
}
@@ -47,12 +47,10 @@ public class FallbackBungeeInjector {
unsafeField.setAccessible(true);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
// Get the base object (the object containing the field)
final Object base = unsafe.staticFieldBase(childField);
// Get the offset of the static field within its class
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer
// Replace the original channel initializer with our new injected initializer
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
File renamed without changes.
@@ -33,8 +33,8 @@ import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Random;
@@ -69,12 +69,12 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
}
}
private final int width, height;
private final @Nullable InputStream background;
private BufferedImage backgroundImage;
private final int width = 128, height = 128;
private final @Nullable File background;
private @Nullable BufferedImage backgroundImage;
@Override
public @NotNull BufferedImage createImage(final char[] answer) {
public @NotNull BufferedImage createImage(final char @NotNull [] answer) {
final BufferedImage image = createBackgroundImage();
final Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
@@ -91,16 +91,22 @@ public final class StandardCaptchaGenerator implements CaptchaGenerator {
}
private @NotNull BufferedImage createBackgroundImage() {
// Create a new image buffer with a 3-byte color spectrum
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
if (background == null) {
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
// Fill the entire image with a noise texture
return FBM.filter(image, image);
}
try {
return backgroundImage == null ? backgroundImage = ImageIO.read(background) : backgroundImage;
} catch (IOException exception) {
throw new IllegalStateException("Could not read background image", exception);
// Try to load the image from a file if it doesn't exist yet
if (backgroundImage == null) {
try {
backgroundImage = ImageIO.read(background);
} catch (IOException exception) {
throw new IllegalStateException("Could not read background image", exception);
}
}
image.createGraphics().drawImage(backgroundImage, 0, 0, Color.WHITE, null);
return image;
}
private void applyRandomColorGradient(final @NotNull Graphics2D graphics) {
@@ -28,7 +28,7 @@ import java.util.Random;
public class Main {
public void main(final String... args) throws IOException {
final Random random = new Random();
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(128, 128, null);
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(null);
final long start = System.currentTimeMillis();
final char[] dictionary = {'a', 'b', 'c', 'd', 'e', 'f'/*, 'g'*/, 'h'/*, 'i'*/, 'j',
File renamed without changes.
@@ -31,30 +31,31 @@ import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.fallback.ratelimit.CaffeineCacheRatelimiter;
import xyz.jonesdev.sonar.common.fallback.ratelimit.NoopCacheRatelimiter;
import xyz.jonesdev.sonar.common.service.ScheduledServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.subcommand.*;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Getter
@RequiredArgsConstructor
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private ActionBarNotificationHandler actionBarNotificationHandler;
@Setter
private Notification notificationHandler;
private ChatNotificationHandler chatNotificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private final LibraryManager libraryManager;
@@ -79,8 +80,8 @@ public abstract class SonarBootstrap<T> implements Sonar {
this.platform = platform;
// Load the rest of the components
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.actionBarNotificationHandler = new ActionBarNotificationHandler();
this.chatNotificationHandler = new ChatNotificationHandler();
this.config = new SonarConfiguration(dataDirectory);
// Register all subcommands
this.subcommandRegistry = new SubcommandRegistry();
@@ -97,13 +98,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
public final void initialize() {
// Check if the branch is not the main branch to warn about unstable versions
if (!getVersion().getGitBranch().equals("main")) {
getLogger().warn("You are currently using an unofficial experimental branch.");
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
}
getLogger().info("Successfully booted in {}s!", launchTimer);
getLogger().info("Initializing shared components...");
@@ -119,7 +113,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Start threads
getLogger().info("Starting all managed threads...");
SonarServiceManager.start();
ScheduledServiceManager.start();
// Done
getLogger().info("Done ({}s)!", launchTimer);
@@ -127,12 +121,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace(System.err);
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released if enabled in the configuration
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
UpdateChecker.checkForUpdates();
}
}
@@ -148,30 +136,14 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Update the CAPTCHA generator if necessary
if (getConfig().getVerification().getMap().getTiming() == SonarConfiguration.Verification.Timing.NEVER) {
getFallback().setCaptchaGenerator(null);
} else if (getFallback().getCaptchaGenerator() == null
|| getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare cached packets
getLogger().info("Taking cached snapshots of all packets...");
FallbackPreparer.prepare();
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.ticker(Ticker.systemTicker())
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.ticker(Ticker.systemTicker())
.build());
// Update ratelimiter cache
getFallback().setRatelimiter(getConfig().getVerification().getReconnectDelay() > 0L
? new CaffeineCacheRatelimiter(getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS)
: NoopCacheRatelimiter.INSTANCE);
// Update blacklist cache
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
@@ -204,7 +176,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Initialize the shutdown process
getLogger().info("Starting shutdown process...");
// Interrupt threads
SonarServiceManager.stop();
ScheduledServiceManager.stop();
// Close database connection if present
if (verifiedPlayerController != null) {
verifiedPlayerController.close();
@@ -25,9 +25,7 @@ import io.netty.channel.ChannelPromise;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -35,27 +33,22 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
// We can only get the size of a message if it's a ByteBuf
public void channelRead(final ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// Increment the incoming traffic by the number of readable bytes
INCOMING.increment(((ByteBuf) msg).readableBytes());
final int readableBytes = ((ByteBuf) msg).readableBytes();
GlobalSonarStatistics.perSecondIncomingTraffic += readableBytes;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
@Override
public void write(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg,
final ChannelPromise promise) throws Exception {
// We can only get the size of a message if it's a ByteBuf
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf) {
// Increment the outgoing traffic by the number of readable bytes
OUTGOING.increment(((ByteBuf) msg).readableBytes());
final int readableBytes = ((ByteBuf) msg).readableBytes();
GlobalSonarStatistics.perSecondOutgoingTraffic += readableBytes;
}
// Make sure to let the server handle the rest
ctx.write(msg, promise);
}
}
@@ -41,7 +41,7 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
// Make sure to let the server handle this
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
@@ -49,26 +49,42 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Make sure to let the server handle this
ctx.fireChannelInactive();
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
// Remove the IP address from the queue
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (ignored, action) -> {
// The player is not queued, so we need to remove them from other maps as well
if (action == null) {
// Remove the IP address from the connected players, if needed
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> {
/*
* Remove this account from the online players or decrement the number of accounts
* with the same IP, but only if the player is logging into the backend server.
* We don't need to decrement the count if the player was just verified since
* we've never actually incremented it in the first place ¯\_(ツ)_/¯
*/
if (v == null) {
Sonar.get().getFallback().getOnline().compute(inetAddress,
(ignored1, count) -> count == null || count <= 1 ? null : count - 1);
}
return null;
});
}
return null;
});
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
/*
* We can override the default exceptionCaught method since this handler
* will run before the connection knows that there has been an error.
*/
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -18,31 +18,27 @@
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.*;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
@AllArgsConstructor
@RequiredArgsConstructor
@@ -51,8 +47,6 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
/**
* Validates and handles incoming handshake packets
*
@@ -64,15 +58,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
throw QuietDecoderException.INSTANCE;
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
throw QuietDecoderException.INSTANCE;
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
@@ -91,63 +83,65 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
throw QuietDecoderException.INSTANCE;
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
throw QuietDecoderException.INSTANCE;
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
final InetAddress inetAddress = socketAddress.getAddress();
channel.pipeline().get(FallbackInboundHandler.class).setInetAddress(inetAddress);
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
if (Sonar.get().getFallback().getConnected().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted);
return;
}
final String hostAddress = inetAddress.getHostAddress();
// Check if the player failed the verification too many times
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
if (limit > 0) {
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
if (score >= limit) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// Make sure we actually have to verify the player
final String offlineUuidString = "OfflinePlayer:" + username;
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inboundHandler.getInetAddress().toString(), offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
if (!Sonar.get().getFallback().getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
@@ -156,9 +150,9 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (__, action) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
if (action != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
// Remove other instances of this IP address from the queue
return null;
@@ -167,17 +161,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
return () -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(username).matches()) {
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUuid);
// Create an instance for the user and let the verification handler take over the channel
new FallbackUserWrapper(channel, inetAddress, protocolVersion, username, fingerprint, geyser);
};
});
}
@@ -216,26 +206,25 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
Sonar.get().getFallback().getOnline().compute(inetAddress, (__, count) -> {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (count != null && maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (count >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return count + 1;
}
}
}
// Let the server know about the login packet
loginPacket.run();
// Let the server know about the login packet
loginPacket.run();
// Increment the number of accounts with the same IP
return count == null ? 1 : count + 1;
});
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
@@ -247,26 +236,28 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
private void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
if (channel.isActive()) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
}
}
}
private void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
// Remove the main pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
// Remove the connection handler pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getConnectionHandler();
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
// Close the channel if no encoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
// We don't need to update the encoder if it has already been replaced by Sonar
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
@@ -0,0 +1,133 @@
/*
* Copyright (C) 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;
import io.netty.channel.Channel;
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 xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarIntLengthEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.fallback.verification.FallbackPreJoinHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.loginSuccess;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
@Getter
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
public final class FallbackUserWrapper implements FallbackUser {
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final String fingerprint;
private final String username;
private final boolean geyser;
private final SystemTimer loginTimer;
@Setter
private boolean forceCaptcha;
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull String username,
final @NotNull String fingerprint,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.username = username;
this.fingerprint = fingerprint;
this.geyser = geyser;
this.loginTimer = new SystemTimer();
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get().getConfig().getVerification().isLogConnections()
&& (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack())) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
// Run this in the channel's event loop to avoid issues
channel.eventLoop().execute(() -> {
// Make sure the channel is still active
if (!channel.isActive()) {
return;
}
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> true);
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarIntLengthEncoder.INSTANCE);
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(loginSuccess);
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
pipeline.addLast(FALLBACK_PACKET_DECODER, newDecoder);
// Listen for all incoming packets by setting the packet listener
newDecoder.setListener(new FallbackPreJoinHandler(this));
// Make sure to catch all exceptions during the verification
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
});
}
@Override
public void disconnect(final @NotNull Component reason) {
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
}
}
@@ -18,14 +18,13 @@
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
@@ -63,11 +62,11 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
// Inject Sonar's channel handler into the pipeline;
// Also make sure the player is not a fake player to avoid compatibility issues
if (channel.isActive() && !FakeChannelUtil.isFakePlayer(channel)) {
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
if (channel.isActive() && !FakeChannelUtil.isFakeChannel(channel)) {
final FallbackInboundHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
@@ -0,0 +1,121 @@
/*
* Copyright (C) 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.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.List;
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
if (!ctx.channel().isActive()) {
byteBuf.clear();
return;
}
// Skip any runs of 0x00 we might find
final int packetStart = byteBuf.forEachByte(FIND_NON_NUL);
if (packetStart == -1) {
return;
}
byteBuf.readerIndex(packetStart);
// Try to read the length of the packet
byteBuf.markReaderIndex();
final int preIndex = byteBuf.readerIndex();
final int length = readRawVarInt21(byteBuf);
if (preIndex == byteBuf.readerIndex()) {
return;
}
if (length <= 0) {
throw DEBUG ? new DecoderException("Bad VarInt length: " + length) : QuietDecoderException.INSTANCE;
}
if (byteBuf.readableBytes() < length) {
byteBuf.resetReaderIndex();
} else {
out.add(byteBuf.readRetainedSlice(length));
}
}
private static int readRawVarInt21(final @NotNull ByteBuf byteBuf) {
if (byteBuf.readableBytes() < 4) {
return readRawVarIntSmallBuffer(byteBuf);
}
// take the last three bytes and check if any of them have the high bit set
final int wholeOrMore = byteBuf.getIntLE(byteBuf.readerIndex());
final int atStop = ~wholeOrMore & 0x808080;
if (atStop == 0) {
throw DEBUG ? new DecoderException("VarInt too big") : QuietDecoderException.INSTANCE;
}
final int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1;
byteBuf.skipBytes(bitsToKeep >> 3);
// https://github.com/netty/netty/pull/14050#issuecomment-2107750734
int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1));
// https://github.com/netty/netty/pull/14050#discussion_r1597896639
preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1);
preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2);
return preservedBytes;
}
private static int readRawVarIntSmallBuffer(final @NotNull ByteBuf byteBuf) {
if (!byteBuf.isReadable()) {
return 0;
}
byteBuf.markReaderIndex();
byte tmp = byteBuf.readByte();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7F;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 7;
}
result |= (tmp & 0x7F) << 7;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 14;
}
return result | (tmp & 0x7F) << 14;
}
}
@@ -20,31 +20,30 @@ package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
public final class FallbackVarIntLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
public static final FallbackVarIntLengthEncoder INSTANCE = new FallbackVarIntLengthEncoder();
@Override
protected void encode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final @NotNull ByteBuf out) throws Exception {
writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
final int length = byteBuf.readableBytes();
final int varIntLength = varIntBytes(length);
@Override
protected ByteBuf allocateBuffer(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
final int anticipatedRequiredCapacity = varIntBytes(msg.readableBytes()) + msg.readableBytes();
return ctx.alloc().buffer(anticipatedRequiredCapacity);
final ByteBuf lenBuf = ctx.alloc().buffer(varIntLength);
writeVarInt(lenBuf, length);
out.add(lenBuf);
out.add(byteBuf.retain());
}
}
@@ -20,15 +20,18 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.maxTotalPacketsSent;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
@@ -36,6 +39,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private FallbackPacketRegistry.ProtocolRegistry registry;
@Setter
private FallbackPacketListener listener;
private int totalPacketsSent;
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
@@ -60,6 +64,11 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
return;
}
// Don't allow the player to spam packets to overload netty
if (++totalPacketsSent > maxTotalPacketsSent) {
throw DEBUG ? new DecoderException("Sent too many packets") : QuietDecoderException.INSTANCE;
}
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
@@ -76,21 +85,20 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion);
} catch (Throwable throwable) {
throw new CorruptedFrameException("Failed to decode packet");
throw DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
throw new CorruptedFrameException("Could not read packet to end");
throw DEBUG ? new DecoderException("Could not read packet to end") : QuietDecoderException.INSTANCE;
}
// Let our verification handler process the packet
if (listener != null) {
listener.handle(packet);
// Make sure to let the timeout handler know about this packet
ctx.fireChannelRead(packet);
}
// Fire channel read to avoid timeout
ctx.fireChannelRead(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
@@ -100,14 +108,14 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int packetSize = byteBuf.readableBytes();
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
throw new CorruptedFrameException("Packet too large");
if (expectedMaxLen != -1 && packetSize > expectedMaxLen) {
throw DEBUG ? new DecoderException("Packet too large: " + packetSize) : QuietDecoderException.INSTANCE;
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw new CorruptedFrameException("Packet too small");
if (expectedMinLen != -1 && packetSize < expectedMinLen) {
throw DEBUG ? new DecoderException("Packet too small: " + packetSize) : QuietDecoderException.INSTANCE;
}
}
}
@@ -50,7 +50,7 @@ public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPa
final ByteBuf out) throws Exception {
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
final int packetId = protocolRegistry.getPacketId(originalPacket);
final int packetId = protocolRegistry.getPacketId(originalPacket.getClass());
writeVarInt(out, packetId);
packet.encode(out, protocolVersion);
}
@@ -20,8 +20,8 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
@@ -32,12 +32,14 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacke
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
HANDSHAKE {
@@ -65,11 +67,11 @@ public enum FallbackPacketRegistry {
map(0x01, MINECRAFT_1_20_2, true),
map(0x02, MINECRAFT_1_20_5, true));
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false),
map(0x03, MINECRAFT_1_20_5, false));
map(0x02, MINECRAFT_1_20_2, true),
map(0x03, MINECRAFT_1_20_5, true));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false),
map(0x04, MINECRAFT_1_20_5, false));
map(0x03, MINECRAFT_1_20_2, true),
map(0x04, MINECRAFT_1_20_5, true));
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
map(0x05, MINECRAFT_1_20_2, true),
map(0x07, MINECRAFT_1_20_5, true));
@@ -105,20 +107,20 @@ public enum FallbackPacketRegistry {
map(0x29, MINECRAFT_1_20_2, true),
map(0x2B, MINECRAFT_1_20_5, true));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x21, MINECRAFT_1_13, false),
map(0x20, MINECRAFT_1_14, false),
map(0x21, MINECRAFT_1_15, false),
map(0x20, MINECRAFT_1_16, false),
map(0x1F, MINECRAFT_1_16_2, false),
map(0x21, MINECRAFT_1_17, false),
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false));
map(0x00, MINECRAFT_1_7_2, true),
map(0x1F, MINECRAFT_1_9, true),
map(0x21, MINECRAFT_1_13, true),
map(0x20, MINECRAFT_1_14, true),
map(0x21, MINECRAFT_1_15, true),
map(0x20, MINECRAFT_1_16, true),
map(0x1F, MINECRAFT_1_16_2, true),
map(0x21, MINECRAFT_1_17, true),
map(0x1E, MINECRAFT_1_19, true),
map(0x20, MINECRAFT_1_19_1, true),
map(0x1F, MINECRAFT_1_19_3, true),
map(0x23, MINECRAFT_1_19_4, true),
map(0x24, MINECRAFT_1_20_2, true),
map(0x26, MINECRAFT_1_20_5, true));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -214,26 +216,24 @@ public enum FallbackPacketRegistry {
map(0x5B, MINECRAFT_1_20_2, true),
map(0x5D, MINECRAFT_1_20_3, true),
map(0x5F, MINECRAFT_1_20_5, true));
clientbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x29, MINECRAFT_1_9, true),
map(0x2B, MINECRAFT_1_13, true),
map(0x2C, MINECRAFT_1_14, true),
map(0x2D, MINECRAFT_1_15, true),
map(0x2C, MINECRAFT_1_16, true),
map(0x2B, MINECRAFT_1_16_2, true),
map(0x2C, MINECRAFT_1_17, true),
map(0x29, MINECRAFT_1_19, true),
map(0x2B, MINECRAFT_1_19_1, true),
map(0x2A, MINECRAFT_1_19_3, true),
map(0x2E, MINECRAFT_1_19_4, true),
map(0x2F, MINECRAFT_1_20_2, true),
map(0x31, MINECRAFT_1_20_5, 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(PlayerInfoPacket.class, PlayerInfoPacket::new,
map(0x38, MINECRAFT_1_7_2, true),
map(0x2D, MINECRAFT_1_9, true),
map(0x2E, MINECRAFT_1_12_1, true),
map(0x30, MINECRAFT_1_13, true),
map(0x33, MINECRAFT_1_14, true),
map(0x34, MINECRAFT_1_15, true),
map(0x33, MINECRAFT_1_16, true),
map(0x32, MINECRAFT_1_16_2, true),
map(0x36, MINECRAFT_1_17, true),
map(0x34, MINECRAFT_1_19, true),
map(0x37, MINECRAFT_1_19_1, true),
map(0x36, MINECRAFT_1_19_3, true),
map(0x3A, MINECRAFT_1_19_4, true),
map(0x3C, MINECRAFT_1_20_2, true),
map(0x3E, MINECRAFT_1_20_5, true));
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
@@ -323,19 +323,19 @@ public enum FallbackPacketRegistry {
map(0x5A, MINECRAFT_1_20_3, true),
map(0x5C, MINECRAFT_1_20_5, true));
clientbound.register(SystemChatPacket.class, SystemChatPacket::new,
map(0x02, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_13, false),
map(0x0F, MINECRAFT_1_15, false),
map(0x0E, MINECRAFT_1_16, false),
map(0x0F, MINECRAFT_1_17, false),
map(0x5F, MINECRAFT_1_19, false),
map(0x62, MINECRAFT_1_19_1, false),
map(0x60, MINECRAFT_1_19_3, false),
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_3, false),
map(0x6C, MINECRAFT_1_20_5, false));
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_17, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true),
map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, true));
clientbound.register(GameEventPacket.class, GameEventPacket::new,
map(0x20, MINECRAFT_1_20_3, true),
map(0x22, MINECRAFT_1_20_5, true));
@@ -358,6 +358,33 @@ public enum FallbackPacketRegistry {
map(0x4F, MINECRAFT_1_20_2, true),
map(0x51, MINECRAFT_1_20_3, true),
map(0x53, MINECRAFT_1_20_5, true));
clientbound.register(EntityAnimationPacket.class, EntityAnimationPacket::new,
map(0x0B, MINECRAFT_1_7_2, true),
map(0x06, MINECRAFT_1_9, true),
map(0x05, MINECRAFT_1_16, true),
map(0x06, MINECRAFT_1_17, true),
map(0x03, MINECRAFT_1_19, true),
map(0x04, MINECRAFT_1_19_4, true),
map(0x03, MINECRAFT_1_20_2, true));
clientbound.register(TeleportEntityPacket.class, TeleportEntityPacket::new,
map(0x18, MINECRAFT_1_7_2, true),
map(0x4A, MINECRAFT_1_9, true),
map(0x49, MINECRAFT_1_9_4, true),
map(0x4B, MINECRAFT_1_12, true),
map(0x4C, MINECRAFT_1_12_1, true),
map(0x50, MINECRAFT_1_13, true),
map(0x56, MINECRAFT_1_14, true),
map(0x57, MINECRAFT_1_15, true),
map(0x56, MINECRAFT_1_16, true),
map(0x61, MINECRAFT_1_17, true),
map(0x62, MINECRAFT_1_18, true),
map(0x63, MINECRAFT_1_19, true),
map(0x66, MINECRAFT_1_19_1, true),
map(0x64, MINECRAFT_1_19_3, true),
map(0x68, MINECRAFT_1_19_4, true),
map(0x6B, MINECRAFT_1_20_2, true),
map(0x6D, MINECRAFT_1_20_3, true),
map(0x70, MINECRAFT_1_20_5, true));
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
map(0x09, MINECRAFT_1_7_2, false),
@@ -402,6 +429,21 @@ public enum FallbackPacketRegistry {
map(0x22, MINECRAFT_1_20_2, false),
map(0x23, MINECRAFT_1_20_3, false),
map(0x26, MINECRAFT_1_20_5, false));
serverbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x10, MINECRAFT_1_9, false),
map(0x11, MINECRAFT_1_12, false),
map(0x10, MINECRAFT_1_12_1, false),
map(0x13, MINECRAFT_1_13, false),
map(0x15, MINECRAFT_1_14, false),
map(0x16, MINECRAFT_1_16, false),
map(0x15, MINECRAFT_1_17, false),
map(0x17, MINECRAFT_1_19, false),
map(0x18, MINECRAFT_1_19_1, false),
map(0x17, MINECRAFT_1_19_3, false),
map(0x18, MINECRAFT_1_19_4, false),
map(0x1A, MINECRAFT_1_20_2, false),
map(0x1B, MINECRAFT_1_20_3, false),
map(0x1E, MINECRAFT_1_20_5, false));
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
@@ -535,6 +577,19 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_20_2, false),
map(0x24, MINECRAFT_1_20_3, false),
map(0x27, MINECRAFT_1_20_5, false));
serverbound.register(AnimationPacket.class, AnimationPacket::new,
map(0x0A, MINECRAFT_1_7_2, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1D, MINECRAFT_1_12, false),
map(0x27, MINECRAFT_1_13, false),
map(0x2A, MINECRAFT_1_14, false),
map(0x2B, MINECRAFT_1_16, false),
map(0x2C, MINECRAFT_1_16_2, false),
map(0x2E, MINECRAFT_1_19, false),
map(0x2F, MINECRAFT_1_19_1, false),
map(0x32, MINECRAFT_1_20_2, false),
map(0x33, MINECRAFT_1_20_3, false),
map(0x36, MINECRAFT_1_20_5, false));
}
};
@@ -551,30 +606,28 @@ public enum FallbackPacketRegistry {
return (direction == Direction.SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version);
}
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
public static final class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> registries = new EnumMap<>(ProtocolVersion.class);
PacketRegistry() {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
for (final ProtocolVersion protocolVersion : ProtocolVersion.values()) {
if (!protocolVersion.isUnknown()) {
registries.put(protocolVersion, new ProtocolRegistry(protocolVersion));
}
}
this.versions = Collections.unmodifiableMap(mutableVersions);
}
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
private @NotNull ProtocolRegistry getProtocolRegistry(final @NotNull ProtocolVersion protocolVersion) {
final ProtocolRegistry registry = registries.get(protocolVersion);
if (registry == null) {
throw new IllegalArgumentException("Could not find data for protocol version " + version);
throw new IllegalArgumentException("Could not find data for protocol version " + protocolVersion);
}
return registry;
}
<P extends FallbackPacket> void register(final Class<P> clazz,
final Supplier<P> packetSupplier,
final PacketMapping @NotNull ... mappings) {
private <T extends FallbackPacket> void register(final @NotNull Class<T> clazz,
final @NotNull Supplier<T> supplier,
final PacketMapping @NotNull ... mappings) {
if (mappings.length == 0) {
throw new IllegalArgumentException("At least one mapping must be provided.");
}
@@ -584,21 +637,21 @@ public enum FallbackPacketRegistry {
final PacketMapping next = (i + 1 < mappings.length) ? mappings[i + 1] : current;
final ProtocolVersion from = current.protocolVersion;
final ProtocolVersion to = getProtocolVersion(current, next);
final ProtocolVersion to = current == next ? LATEST_VERSION : next.protocolVersion;
for (final ProtocolVersion protocol : EnumSet.range(from, to)) {
if (protocol == to && next != current) {
for (final ProtocolVersion protocolVersion : EnumSet.range(from, to)) {
if (protocolVersion == to && next != current) {
break;
}
final ProtocolRegistry registry = versions.get(protocol);
final ProtocolRegistry registry = registries.get(protocolVersion);
if (registry == null) {
throw new IllegalArgumentException("Unknown protocol version " + from);
throw new IllegalArgumentException("Unknown protocol version " + protocolVersion);
}
if (registry.packetIdToSupplier.containsKey(current.id)) {
if (registry.packetIdToSupplier.containsKey(current.packetId)) {
throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName()
+ " with id " + current.id + " for " + registry.protocolVersion
+ " with id " + current.packetId + " for " + registry.protocolVersion
+ " because another packet is already registered");
}
@@ -608,18 +661,12 @@ public enum FallbackPacketRegistry {
}
if (!current.encodeOnly) {
registry.packetIdToSupplier.put(current.id, packetSupplier);
registry.packetIdToSupplier.put(current.packetId, supplier);
}
registry.packetClassToId.put(clazz, current.id);
registry.packetClassToId.put(clazz, current.packetId);
}
}
}
@NotNull
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
final @NotNull PacketMapping next) {
return current == next ? LATEST_VERSION : next.protocolVersion;
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@@ -630,43 +677,38 @@ public enum FallbackPacketRegistry {
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
new HashMap<>(16, 0.5f);
public FallbackPacket createPacket(final int id) {
final Supplier<? extends FallbackPacket> supplier = packetIdToSupplier.get(id);
if (supplier == null) {
return null;
}
return supplier.get();
public FallbackPacket createPacket(final int packetId) {
final var supplier = packetIdToSupplier.get(packetId);
return supplier == null ? null : supplier.get();
}
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
throw new IllegalArgumentException("Could not find packet");
public int getPacketId(final @NotNull Class<? extends FallbackPacket> clazz) {
final int packetId = packetClassToId.getOrDefault(clazz, -1);
if (packetId == -1) {
throw new IllegalArgumentException("Could not find packet ID for " + clazz.getSimpleName());
}
return id;
return packetId;
}
}
@Data
@ToString
public static final class PacketMapping {
private final int id;
private final int packetId;
private final ProtocolVersion protocolVersion;
private final boolean encodeOnly;
PacketMapping(final int id,
PacketMapping(final int packetId,
final ProtocolVersion protocolVersion,
final boolean encodeOnly) {
this.id = id;
this.packetId = packetId;
this.protocolVersion = protocolVersion;
this.encodeOnly = encodeOnly;
}
}
private static @NotNull PacketMapping map(final int id,
private static @NotNull PacketMapping map(final int packetId,
final ProtocolVersion protocolVersion,
final boolean encodeOnly) {
return new PacketMapping(id, protocolVersion, encodeOnly);
return new PacketMapping(packetId, protocolVersion, encodeOnly);
}
}
@@ -29,7 +29,6 @@ import java.util.Map;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
@Getter
public final class FallbackPacketSnapshot implements FallbackPacket {
@@ -18,10 +18,14 @@
package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.IntBinaryTag;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationStartEvent;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
@@ -33,50 +37,57 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPac
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;
@UtilityClass
public class FallbackPreparer {
private final SecureRandom RANDOM = new SecureRandom();
// LoginSuccess
public FallbackPacket loginSuccess;
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x06, 0f, 0f));
// Chunks
private final Random RANDOM = new Random();
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt(50);
public static final int VEHICLE_BOAT_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
public static final int VEHICLE_MINECART_ENTITY_ID = VEHICLE_BOAT_ENTITY_ID + 1 + RANDOM.nextInt(10);
public static final CompoundBinaryTag MAP_ITEM_NBT = CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
.build();
public static final BlockType[] POSSIBLE_BLOCK_TYPES = BlockType.values();
public final FallbackPacket[] BLOCKS_PACKETS = new FallbackPacket[POSSIBLE_BLOCK_TYPES.length];
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (vanilla protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int DEFAULT_Y_COLLIDE_POSITION = 1 + RANDOM.nextInt(255); // 255 is the maximum Y position
public final int IN_AIR_Y_POSITION = 1000 + RANDOM.nextInt(338); // High altitude (randomized)
public final int IN_VOID_Y_POSITION = -(120 + RANDOM.nextInt(120)); // Low altitude (randomized)
public final int FIRST_TELEPORT_ID = RANDOM.nextInt();
public final int SECOND_TELEPORT_ID = RANDOM.nextInt();
public final FallbackPacket DEFAULT_ABILITIES = new PlayerAbilitiesPacket(0x00, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES = new PlayerAbilitiesPacket(0x02, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES_BEDROCK = new PlayerAbilitiesPacket(0x06, 0, 0);
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, 0, false));
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket(); // No snapshot needed because it's empty either way
// Synchronize Registry
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
public final FallbackPacket REGISTRY_SYNC_LEGACY = new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null));
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
// Game Event (1.20.3+)
public final FallbackPacket START_WRITING_CHUNKS = new FallbackPacketSnapshot(new GameEventPacket(13, 0));
// Chat
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
public final static FallbackPacket CACHED_HELD_ITEM_SLOT = new SetHeldItemPacket(~4);
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(RANDOM.nextInt());
public static FallbackPacket loginSuccess;
public FallbackPacket enterCodeMessage;
public FallbackPacket incorrectCaptcha;
// JoinGame
public final int PLAYER_ENTITY_ID = RANDOM.nextInt(10);
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
// Player Info
public final FallbackPacket PLAYER_INFO = new FallbackPacketSnapshot(new PlayerInfoPacket(
"", new UUID(1L, 1L), 2));
// Default Spawn Position
public static FallbackPacket joinGame;
public FallbackPacket defaultSpawnPosition;
// Spawn Position
public final int TELEPORT_ID = RANDOM.nextInt();
public FallbackPacket spawnPosition;
// Transfer packet
public FallbackPacket fallStartPosition;
public FallbackPacket fallStartPositionLegacy;
public static FallbackPacket transferToOrigin;
// Disconnect messages
public FallbackPacket blacklisted;
public FallbackPacket alreadyQueued;
public FallbackPacket alreadyVerifying;
@@ -84,33 +95,16 @@ public class FallbackPreparer {
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
public FallbackPacket tooManyOnlinePerIP;
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket spawnEntity;
public static FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
// 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
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public final int IN_AIR_Y_POSITION = 1337;
// CAPTCHA position
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, false));
// Platform
public BlockType blockType;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
// XP packets
public FallbackPacket removeBoat;
public FallbackPacket teleportMinecart;
public FallbackPacket spawnBoatEntity;
public FallbackPacket spawnMinecartEntity;
public FallbackPacket setBoatPassengers;
public FallbackPacket setMinecartPassengers;
public FallbackPacket[] xpCountdown;
public static int maxMovementTick, dynamicSpawnYPosition, maxTotalPacketsSent;
@SuppressWarnings("ResultOfMethodCallIgnored")
public void prepare() {
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
@@ -118,54 +112,56 @@ public class FallbackPreparer {
FallbackPacketRegistry.values();
// Prepare LoginSuccess packet
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L),
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(UUID.randomUUID(),
Sonar.get().getConfig().getGeneralConfig().getString("verification.cached-username")));
// Prepare JoinGame packet
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get().getConfig().getVerification().getGamemode().getId(),
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
Sonar.get().getConfig().getVerification().getGamemode().getId(), RANDOM.nextLong(),
0, 0, false, true, false,
false, false, false, true,
new String[]{"minecraft:overworld"}, "minecraft:overworld", "flat"));
// Prepare cached motions for the gravity check
maxFallDistance = 0;
// Prepare the gravity check
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
preparedCachedYMotions = new double[maxMovementTick + 8];
for (int i = 0; i < preparedCachedYMotions.length; i++) {
final double gravity = -((Math.pow(0.98, i) - 1) * 3.92);
preparedCachedYMotions[i] = gravity;
maxFallDistance += gravity;
double motionY = 0, fallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
motionY = (motionY - 0.1) * 0.98f;
fallDistance += Math.abs(motionY);
}
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(fallDistance);
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, FIRST_TELEPORT_ID, 0, false));
fallStartPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition - IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, -90, SECOND_TELEPORT_ID, 1 << 1, false));
fallStartPositionLegacy = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0, -90, TELEPORT_ID, false));
0, -90, 0, 0, false));
// Prepare collision platform positions
blockType = BlockType.valueOf(Sonar.get().getConfig().getGeneralConfig().getString(
"verification.checks.collision.collision-block-type").toUpperCase());
final BlockUpdate[] changedBlocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
final int length = BLOCKS_PER_ROW / 2;
for (int i = 0; i < BLOCKS_PACKETS.length; i++) {
final BlockUpdate[] blocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + (BLOCKS_PER_ROW / 2),
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0);
changedBlocks[index++] = new BlockUpdate(position, blockType);
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + length, DEFAULT_Y_COLLIDE_POSITION, z + length, 0, 0);
blocks[index++] = new BlockUpdate(position, POSSIBLE_BLOCK_TYPES[i]);
}
}
BLOCKS_PACKETS[i] = new UpdateSectionBlocksPacket(0, 0, blocks);
}
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
// Prepare disconnect packets during login
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
alreadyVerifying = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
@@ -185,10 +181,17 @@ public class FallbackPreparer {
}
// Prepare packets for the vehicle check
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
spawnEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
removeBoat = new RemoveEntitiesPacket(VEHICLE_BOAT_ENTITY_ID);
teleportMinecart = new TeleportEntityPacket(
VEHICLE_MINECART_ENTITY_ID, SPAWN_X_POSITION, IN_VOID_Y_POSITION, SPAWN_Z_POSITION, false);
spawnBoatEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(VEHICLE_BOAT_ENTITY_ID,
EntityType.BOAT, RANDOM.nextInt(16), IN_AIR_Y_POSITION, RANDOM.nextInt(16),
0, 0, 0, 0));
spawnMinecartEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(VEHICLE_MINECART_ENTITY_ID,
EntityType.MINECART, RANDOM.nextInt(16), IN_AIR_Y_POSITION, RANDOM.nextInt(16),
0, 0, 0, 0));
setBoatPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_BOAT_ENTITY_ID, PLAYER_ENTITY_ID));
setMinecartPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_MINECART_ENTITY_ID, PLAYER_ENTITY_ID));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
@@ -210,6 +213,19 @@ public class FallbackPreparer {
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, 0));
}
// Update the CAPTCHA generator if necessary
final CaptchaGenerationStartEvent generationStartEvent = new CaptchaGenerationStartEvent(
Sonar.get().getFallback().getCaptchaGenerator());
Sonar.get().getEventManager().publish(generationStartEvent);
if (generationStartEvent.getCaptchaGenerator() == null
|| generationStartEvent.getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
Sonar.get().getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
Sonar.get().getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare CAPTCHA answers
CaptchaPreparer.prepare();
} else {
@@ -217,6 +233,13 @@ public class FallbackPreparer {
enterCodeMessage = null;
incorrectCaptcha = null;
xpCountdown = null;
Sonar.get().getFallback().setCaptchaGenerator(null);
}
maxTotalPacketsSent = maxMovementTick + 2
+ (xpCountdown == null ? 0 : xpCountdown.length) * 20 + 5
+ Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries()
+ 150 /* some arbitrary leeway */;
}
}
@@ -0,0 +1,293 @@
/*
* Copyright (C) 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.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
//STONE(protocolVersion -> 1, 1),
ENCHANTMENT_TABLE(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 116;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 4612;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 4613;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5116;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5132;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5136;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5333;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 5719;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7159;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7385;
}
return 7389;
}, 0.75f),
TRAPDOOR(protocolVersion -> {
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
if (protocolVersion.compareTo(MINECRAFT_1_7_6) <= 0) {
return 96;
}
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 167;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 6509;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 6510;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7016;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 7552;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 7556;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 7802;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 8293;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 9937;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 10269;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 10273;
}
return 10414;
}, 0.1875),
END_PORTAL_FRAME(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 120;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 4633;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 4634;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5137;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5153;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5157;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5358;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 5744;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7184;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7410;
}
return 7414;
}, 0.8125),
DAYLIGHT_SENSOR(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 151;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 5651;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 5652;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 6158;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 6694;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 6698;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 6916;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 7327;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 8811;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 9063;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 9067;
}
return 9207;
}, 0.375),
COBBLESTONE_WALL(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 139;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 5196;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 5197;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5700;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5660;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5664;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5866;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 6252;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7692;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7918;
}
return 7922;
}, 1.5),
STONE_SLABS(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 44;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 7296;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 7297;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7809;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 8345;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 8349;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 8595;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 9092;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 10748;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 11086;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 11090;
}
return 11231;
}, 0.5),
WHITE_CARPET(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 171;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 6823;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 6824;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7330;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 7866;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 7870;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 8116;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 8607;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 10251;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 10583;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 10587;
}
return 10728;
}, 0.0625);
private final Function<ProtocolVersion, Integer> idFunction;
@Getter
private final double blockHeight;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return idFunction.apply(protocolVersion);
}
}
@@ -25,12 +25,12 @@ import org.jetbrains.annotations.NotNull;
@Getter
public final class BlockUpdate {
private final @NotNull BlockPosition position;
private final BlockType type;
private final BlockType blockType;
private final int legacyBlockState, blockState;
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
public BlockUpdate(final @NotNull BlockPosition position, final BlockType blockType) {
this.position = position;
this.type = type;
this.blockType = blockType;
final int x = position.getX() - (position.getChunkX() << 4);
final int y = position.getY();
final int z = position.getZ() - (position.getChunkZ() << 4);
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.CaptchaGenerationEndEvent;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.image.BufferedImage;
@@ -54,18 +55,18 @@ public class CaptchaPreparer {
// Prepare everything asynchronously
PREPARATION_SERVICE.execute(() -> {
for (preparedAmount = 0; preparedAmount < precomputeAmount;) {
// Generate CAPTCHA answer
// Generate CAPTCHA
final char[] answer = new char[3 + RANDOM.nextInt(2)];
for (int j = 0; j < answer.length; j++) {
answer[j] = alphabet[RANDOM.nextInt(alphabet.length)];
}
final BufferedImage image = Sonar.get().getFallback().getCaptchaGenerator().createImage(answer);
// Convert and cache converted Minecraft map bytes
final int[] buffer = MapColorPalette.getBufferFromImage(image);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), buffer);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), MapColorPalette.imageToBuffer(image));
}
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
Sonar.get().getEventManager().publish(new CaptchaGenerationEndEvent(timer, preparedAmount));
});
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 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.captcha;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@RequiredArgsConstructor
public enum ItemType {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
FILLED_MAP(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 358;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 608;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 613;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 671;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 733;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 847;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 886;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 914;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 937;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
return 941;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
return 979;
}
return 982;
});
private final Function<ProtocolVersion, Integer> idFunction;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return idFunction.apply(protocolVersion);
}
}
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff. Show more