Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59731a93f1 | ||
|
|
ace55fbe7b | ||
|
|
696d3c5c81 | ||
|
|
a1e4ea3f5b | ||
|
|
6b054bd56c | ||
|
|
c8e2142dd9 | ||
|
|
1cf7ef6994 | ||
|
|
af8aa103bf | ||
|
|
d1f2c15a8f | ||
|
|
5a68c4d557 | ||
|
|
1438856531 | ||
|
|
694725b798 | ||
|
|
47cb4d0840 | ||
|
|
38ecb9ac06 | ||
|
|
6657a3d674 | ||
|
|
823ee21cea | ||
|
|
4d206af0ae | ||
|
|
2ff7b7efaa | ||
|
|
a5efc1c796 | ||
|
|
85d5ce77b1 | ||
|
|
6c4d4b79d6 | ||
|
|
364b1c5b31 | ||
|
|
4edf04ede5 | ||
|
|
01da1a70ea | ||
|
|
9bb7c7cd60 | ||
|
|
aaa7588dcd | ||
|
|
8d378d56ef | ||
|
|
476b1704b8 | ||
|
|
26c31ceeaf | ||
|
|
40f65ef5eb | ||
|
|
7736ccff86 | ||
|
|
437a547ec3 | ||
|
|
4ec65218da | ||
|
|
de45e9f808 | ||
|
|
87e768da0e | ||
|
|
daa773bff1 | ||
|
|
1e43835d0b | ||
|
|
efb8b3cd39 | ||
|
|
540899e92c | ||
|
|
ef6140d47f | ||
|
|
27ca5a8368 | ||
|
|
1a43242c24 | ||
|
|
65eeec6e17 | ||
|
|
d8e4dbd6cf | ||
|
|
32973e674f | ||
|
|
05fa6de119 | ||
|
|
cf0f4e0b96 | ||
|
|
0ae3c6c67b | ||
|
|
aae564d16c | ||
|
|
f6338ba844 | ||
|
|
d892645779 | ||
|
|
def40be5c0 | ||
|
|
d4bdf86fcb |
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.1.2
|
||||
version=2.1.7
|
||||
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
netty = "4.1.112.Final"
|
||||
netty = "4.1.113.Final"
|
||||
adventure = "4.17.0"
|
||||
adventure-platform = "4.3.4"
|
||||
bstats = "3.0.3"
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
@@ -216,7 +197,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");
|
||||
@@ -303,7 +283,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 +297,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
|
||||
@@ -434,7 +414,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 +468,7 @@ public final class SonarConfiguration {
|
||||
private final DatabaseType databaseType;
|
||||
private final Library databaseDriver;
|
||||
@Setter
|
||||
private boolean downloaded;
|
||||
private boolean loaded;
|
||||
}
|
||||
|
||||
private Type type;
|
||||
|
||||
+36
-115
@@ -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,13 @@ 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) {
|
||||
ignored.printStackTrace();
|
||||
/*
|
||||
* 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 +117,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 +136,7 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
connectionSource.close();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error("Error closing database: {}", exception);
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,11 +153,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 +166,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 +185,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 +194,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 +206,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 +223,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 +237,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 +249,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);
|
||||
}
|
||||
}
|
||||
+7
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
@@ -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
|
||||
|
||||
+2
-5
@@ -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;
|
||||
}
|
||||
+11
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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) {
|
||||
@@ -26,7 +26,7 @@ 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;
|
||||
@@ -40,35 +40,20 @@ public final class 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);
|
||||
// TODO: Is there a way to improve this?
|
||||
// (E.g. no map needed without concurrency issues)
|
||||
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(256);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@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());
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackRatelimiter {
|
||||
private long timeout;
|
||||
private Cache<InetAddress, Long> attemptCache;
|
||||
@Getter
|
||||
private Cache<InetAddress, Integer> failCountCache;
|
||||
|
||||
/**
|
||||
* Attempts to rate-limit the client
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @return Whether the player is allowed to verify
|
||||
*/
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
final long expectedTimestamp = System.currentTimeMillis() + timeout;
|
||||
// Check if the time since the last join has not exceeded the timeout
|
||||
// The idea for this check was Taken from Velocity
|
||||
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
|
||||
return expectedTimestamp == last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player is failing verifications too often
|
||||
* Increments the number of times this user has failed the verification
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @param count Count of previously failed verifications
|
||||
*/
|
||||
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
|
||||
// Make sure to remove the old values from the cache
|
||||
if (count > 0) {
|
||||
failCountCache.invalidate(inetAddress);
|
||||
}
|
||||
failCountCache.put(inetAddress, count + 1);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+5
-6
@@ -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();
|
||||
|
||||
+5
-5
@@ -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;
|
||||
+5
-5
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
+3
-3
@@ -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
|
||||
+2
-2
@@ -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'};
|
||||
+4
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+22
-4
@@ -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
-18
@@ -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;
|
||||
}
|
||||
|
||||
+18
-17
@@ -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) {
|
||||
|
||||
+1
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -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);
|
||||
|
||||
@@ -31,30 +31,32 @@ 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.fallback.ratelimit.CaffeineCacheRatelimiter;
|
||||
import xyz.jonesdev.sonar.common.fallback.ratelimit.NoopCacheRatelimiter;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommands.*;
|
||||
import xyz.jonesdev.sonar.common.subcommand.*;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
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 +81,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();
|
||||
@@ -148,30 +150,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();
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ 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 static xyz.jonesdev.sonar.common.statistics.BandwidthStatistics.INCOMING;
|
||||
import static xyz.jonesdev.sonar.common.statistics.BandwidthStatistics.OUTGOING;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
|
||||
+27
-11
@@ -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();
|
||||
}
|
||||
}
|
||||
+60
-69
@@ -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);
|
||||
|
||||
+33
-128
@@ -18,39 +18,34 @@
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
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.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.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.FallbackVarInt21FrameEncoder;
|
||||
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.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.verification.FallbackPreJoinHandler;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
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"})
|
||||
@@ -59,50 +54,38 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final UUID offlineUuid;
|
||||
@Setter
|
||||
private boolean receivedClientSettings;
|
||||
@Setter
|
||||
private boolean receivedPluginMessage;
|
||||
private final String fingerprint;
|
||||
private final String username;
|
||||
private final boolean geyser;
|
||||
private final SystemTimer loginTimer = new SystemTimer();
|
||||
private final SystemTimer loginTimer;
|
||||
@Setter
|
||||
private boolean forceCaptcha;
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull UUID offlineUuid,
|
||||
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.offlineUuid = offlineUuid;
|
||||
this.username = username;
|
||||
this.fingerprint = fingerprint;
|
||||
this.geyser = geyser;
|
||||
}
|
||||
this.loginTimer = new SystemTimer();
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
|
||||
// The player has joined the verification
|
||||
GlobalSonarStatistics.totalAttemptedVerifications++;
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().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())));
|
||||
}
|
||||
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
|
||||
@@ -116,25 +99,25 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
// Mark the player as connected by caching them in a map of verifying players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> (byte) 0);
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarInt21FrameEncoder.INSTANCE);
|
||||
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(FallbackPreparer.loginSuccess);
|
||||
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 fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
|
||||
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
pipeline.addLast(FALLBACK_PACKET_DECODER, newDecoder);
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
|
||||
newDecoder.setListener(new FallbackPreJoinHandler(this));
|
||||
|
||||
// Make sure to catch all exceptions during the verification
|
||||
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
|
||||
@@ -142,87 +125,9 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(final @NotNull String reason) {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
// Only log the failed message if the server isn't currently under attack.
|
||||
// However, we let the user override this through the configuration.
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("<reason>", reason));
|
||||
}
|
||||
|
||||
// Increment number of total failed verifications
|
||||
GlobalSonarStatistics.totalFailedVerifications++;
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method in the future
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Use 1 as the default amount of fails since we haven't cached anything yet
|
||||
final int fails = Sonar.get().getFallback().getRatelimiter().getFailCountCache().get(inetAddress, ignored -> 1);
|
||||
// Now we simply need to check if the threshold is reached
|
||||
if (fails < blacklistThreshold) {
|
||||
// Make sure we increment the number of fails
|
||||
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, fails);
|
||||
break blacklist;
|
||||
}
|
||||
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
// Increment number of total blacklisted players
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
}
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw new CorruptedFrameException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel Channel to close
|
||||
* @param protocolVersion Protocol version
|
||||
* @param msg Packet to close the channel with
|
||||
*/
|
||||
public static void closeWith(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull Object msg) {
|
||||
if (channel.isActive()) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
channel.config().setAutoRead(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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);
|
||||
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* 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.util.ByteProcessor;
|
||||
import lombok.Getter;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
|
||||
@Getter
|
||||
final class FallbackVarInt21ByteDecoder implements ByteProcessor {
|
||||
private int readVarInt, bytesRead;
|
||||
private DecodeResult result = DecodeResult.TOO_SHORT;
|
||||
|
||||
@Override
|
||||
public boolean process(byte k) {
|
||||
if (k == 0 && bytesRead == 0) {
|
||||
result = DecodeResult.RUN_OF_ZEROES;
|
||||
return true;
|
||||
}
|
||||
if (result == DecodeResult.RUN_OF_ZEROES) {
|
||||
return false;
|
||||
}
|
||||
readVarInt |= (k & 0x7F) << bytesRead++ * 7;
|
||||
if (bytesRead > 3) {
|
||||
result = DecodeResult.TOO_BIG;
|
||||
return false;
|
||||
}
|
||||
if ((k & 0x80) != 128) {
|
||||
result = DecodeResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum DecodeResult {
|
||||
SUCCESS,
|
||||
TOO_SHORT,
|
||||
TOO_BIG,
|
||||
RUN_OF_ZEROES
|
||||
}
|
||||
}
|
||||
+80
-36
@@ -20,58 +20,102 @@ 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.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Taken from
|
||||
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 ByteBuf in, final List<Object> out) {
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive()) {
|
||||
in.clear();
|
||||
byteBuf.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final FallbackVarInt21ByteDecoder reader = new FallbackVarInt21ByteDecoder();
|
||||
final int end = in.forEachByte(reader);
|
||||
|
||||
if (end == -1) {
|
||||
// We tried to go beyond the end of the buffer.
|
||||
// This is probably a good sign that the buffer was too short to hold a proper varint.
|
||||
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
|
||||
// Special case where the entire packet is just a run of zeroes. We ignore them all.
|
||||
in.clear();
|
||||
}
|
||||
// Skip any runs of 0x00 we might find
|
||||
final int packetStart = byteBuf.forEachByte(FIND_NON_NUL);
|
||||
if (packetStart == -1) {
|
||||
return;
|
||||
}
|
||||
byteBuf.readerIndex(packetStart);
|
||||
|
||||
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
|
||||
// This will return to the point where the next varint starts
|
||||
in.readerIndex(end);
|
||||
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.SUCCESS) {
|
||||
final int readVarInt = reader.getReadVarInt();
|
||||
final int bytesRead = reader.getBytesRead();
|
||||
// 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 (readVarInt < 0) {
|
||||
in.clear();
|
||||
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
|
||||
} else if (readVarInt == 0) {
|
||||
// Skip over the empty packet(s) and ignore it
|
||||
in.readerIndex(end + 1);
|
||||
} else {
|
||||
int minimumRead = bytesRead + readVarInt;
|
||||
if (in.isReadable(minimumRead)) {
|
||||
out.add(in.retainedSlice(end + 1, readVarInt));
|
||||
in.skipBytes(minimumRead);
|
||||
}
|
||||
}
|
||||
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
|
||||
in.clear();
|
||||
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
|
||||
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;
|
||||
}
|
||||
}
|
||||
+14
-15
@@ -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());
|
||||
}
|
||||
}
|
||||
+17
-8
@@ -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,12 +85,12 @@ 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
|
||||
@@ -100,14 +109,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-17
@@ -37,7 +37,6 @@ 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 {
|
||||
@@ -214,26 +213,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, false),
|
||||
map(0x2B, MINECRAFT_1_13, false),
|
||||
map(0x2C, MINECRAFT_1_14, false),
|
||||
map(0x2D, MINECRAFT_1_15, false),
|
||||
map(0x2C, MINECRAFT_1_16, false),
|
||||
map(0x2B, MINECRAFT_1_16_2, false),
|
||||
map(0x2C, MINECRAFT_1_17, false),
|
||||
map(0x29, MINECRAFT_1_19, false),
|
||||
map(0x2B, MINECRAFT_1_19_1, false),
|
||||
map(0x2A, MINECRAFT_1_19_3, false),
|
||||
map(0x2E, MINECRAFT_1_19_4, false),
|
||||
map(0x2F, MINECRAFT_1_20_2, false),
|
||||
map(0x31, MINECRAFT_1_20_5, false));
|
||||
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),
|
||||
@@ -358,6 +355,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 +426,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 +574,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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
-1
@@ -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 {
|
||||
|
||||
+102
-86
@@ -18,10 +18,13 @@
|
||||
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.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 +36,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_RESET = 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 +94,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 +111,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,
|
||||
RANDOM.nextLong(), false, 0,
|
||||
true, false, false,
|
||||
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
|
||||
|
||||
// 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 = 1;
|
||||
|
||||
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 +180,15 @@ 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, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
|
||||
spawnMinecartEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
|
||||
VEHICLE_MINECART_ENTITY_ID, EntityType.MINECART, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
|
||||
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 +210,15 @@ public class FallbackPreparer {
|
||||
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, 0));
|
||||
}
|
||||
|
||||
// Update the CAPTCHA generator if necessary
|
||||
if (Sonar.get().getFallback().getCaptchaGenerator() == null
|
||||
|| Sonar.get().getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
|
||||
Sonar.get().getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
|
||||
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 +226,13 @@ public class FallbackPreparer {
|
||||
enterCodeMessage = null;
|
||||
incorrectCaptcha = null;
|
||||
xpCountdown = null;
|
||||
Sonar.get().getFallback().setCaptchaGenerator(null);
|
||||
}
|
||||
|
||||
maxTotalPacketsSent = maxMovementTick
|
||||
+ (xpCountdown == null ? 0 : xpCountdown.length) * 3
|
||||
+ Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries()
|
||||
+ 150 /* some arbitrary leeway */;
|
||||
}
|
||||
}
|
||||
+244
-145
@@ -24,167 +24,266 @@ 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 {
|
||||
STONE(protocolVersion -> 1, 1),
|
||||
// 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 -> {
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
default:
|
||||
// 1.7.2-1.12.2
|
||||
return 116;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 4612;
|
||||
case MINECRAFT_1_13_2:
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
return 4613;
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
case MINECRAFT_1_16:
|
||||
return 5116;
|
||||
case MINECRAFT_1_16_1:
|
||||
return 5132;
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 5136;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 5333;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 5719;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 7159;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 7385;
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 7389;
|
||||
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 -> {
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
|
||||
return 96;
|
||||
default:
|
||||
// 1.8-1.12.2
|
||||
return 167;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 6509;
|
||||
case MINECRAFT_1_13_2:
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
return 6510;
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
case MINECRAFT_1_16:
|
||||
return 7016;
|
||||
case MINECRAFT_1_16_1:
|
||||
return 7552;
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 7556;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7802;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 8293;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 9937;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 10269;
|
||||
case MINECRAFT_1_20:
|
||||
return 10273;
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10414;
|
||||
// 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),
|
||||
BARRIER(protocolVersion -> {
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
|
||||
return 20;
|
||||
default:
|
||||
// 1.8-1.12.2
|
||||
return 166;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 6493;
|
||||
case MINECRAFT_1_13_2:
|
||||
return 6494;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 7000;
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
return 7536;
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 7540;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7754;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 8245;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 9889;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 10221;
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10366;
|
||||
END_PORTAL_FRAME(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
|
||||
return 120;
|
||||
}
|
||||
}, 1);
|
||||
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;
|
||||
// TODO: Implement per-version block heights?
|
||||
@Getter
|
||||
private final double blockHeight;
|
||||
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+2
-3
@@ -54,15 +54,14 @@ 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);
|
||||
|
||||
+42
-51
@@ -23,64 +23,55 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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 -> {
|
||||
// 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
|
||||
switch (protocolVersion) {
|
||||
default:
|
||||
// 1.7-1.12.2
|
||||
return 358;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 608;
|
||||
case MINECRAFT_1_13_2:
|
||||
return 613;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 671;
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 733;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 847;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 886;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 914;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 937;
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
return 941;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 982;
|
||||
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> function;
|
||||
private final Function<ProtocolVersion, Integer> idFunction;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
return idFunction.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -32,14 +32,13 @@ public final class MapCaptchaInfo {
|
||||
private final FallbackPacket[] legacy;
|
||||
private final FallbackPacket modern;
|
||||
|
||||
public MapCaptchaInfo(final @NotNull String answer, final int @NotNull [] buffer) {
|
||||
public MapCaptchaInfo(final @NotNull String answer, final byte @NotNull [] buffer) {
|
||||
this.answer = answer;
|
||||
|
||||
// Prepare 1.7 map data using a grid
|
||||
final int[][] grid = new int[128][128];
|
||||
final byte[][] grid = new byte[128][128];
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
final int buf = buffer[i];
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buffer[i];
|
||||
}
|
||||
this.legacy = new FallbackPacket[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
|
||||
+20
-12
@@ -21,6 +21,7 @@ import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.Raster;
|
||||
|
||||
@UtilityClass
|
||||
public class MapColorPalette {
|
||||
@@ -172,25 +173,32 @@ public class MapColorPalette {
|
||||
-12910336,
|
||||
};
|
||||
|
||||
public int[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
|
||||
final int[] buffer = new int[128 * 128];
|
||||
|
||||
bufferedImage.getRGB(0, 0,
|
||||
bufferedImage.getWidth(), bufferedImage.getHeight(),
|
||||
buffer, 0, bufferedImage.getWidth());
|
||||
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = mapColorToMinecraft(buffer[i]);
|
||||
}
|
||||
public byte[] imageToBuffer(final @NotNull BufferedImage bufferedImage) {
|
||||
final byte[] buffer = new byte[128 * 128];
|
||||
writeRGBBuffer(bufferedImage, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte mapColorToMinecraft(final int argb) {
|
||||
private void writeRGBBuffer(final @NotNull BufferedImage bufferedImage, final byte @NotNull [] rgbArray) {
|
||||
final Raster raster = bufferedImage.getRaster();
|
||||
final Object data = new byte[raster.getNumBands()];
|
||||
int yoff = 0, off;
|
||||
|
||||
for (int y = 0; y < raster.getHeight(); y++, yoff += raster.getWidth()) {
|
||||
off = yoff;
|
||||
for (int x = 0; x < raster.getWidth(); x++) {
|
||||
final int rgb = bufferedImage.getColorModel().getRGB(raster.getDataElements(x, y, data));
|
||||
rgbArray[off++] = rgbToNearestMinecraft(rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte rgbToNearestMinecraft(final int rgb) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
byte closestIndex = 0;
|
||||
for (int i = 0; i < COLOR_MAP.length; i++) {
|
||||
final byte index = (byte) (i < 128 ? i : -129 + (i - 127));
|
||||
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
|
||||
final int distance = calculateColorDistance(rgb, COLOR_MAP[i]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = index;
|
||||
|
||||
+17
-17
@@ -30,38 +30,32 @@ import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
public final class DimensionRegistry {
|
||||
public final CompoundBinaryTag CODEC_1_16;
|
||||
public final CompoundBinaryTag CODEC_1_16_2;
|
||||
public final CompoundBinaryTag CODEC_1_18_2;
|
||||
public final CompoundBinaryTag CODEC_1_19;
|
||||
public final CompoundBinaryTag CODEC_1_19_1;
|
||||
public final CompoundBinaryTag CODEC_1_19_4;
|
||||
public final CompoundBinaryTag CODEC_1_20;
|
||||
public final CompoundBinaryTag CODEC_1_21;
|
||||
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_16;
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
|
||||
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_16;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_20;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_21;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
|
||||
static {
|
||||
CODEC_1_16 = getCodec("codec_1_16.nbt");
|
||||
CODEC_1_16_2 = getCodec("codec_1_16_2.nbt");
|
||||
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
|
||||
CODEC_1_19 = getCodec("codec_1_19.nbt");
|
||||
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
|
||||
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
|
||||
CODEC_1_20 = getCodec("codec_1_20.nbt");
|
||||
CODEC_1_21 = getCodec("codec_1_21.nbt");
|
||||
OLD_CODEC = getCodec("codec_old.nbt");
|
||||
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16_2);
|
||||
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
|
||||
}
|
||||
|
||||
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
|
||||
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
|
||||
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
|
||||
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
@@ -70,4 +64,10 @@ public final class DimensionRegistry {
|
||||
throw new IllegalStateException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
|
||||
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
|
||||
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
|
||||
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
|
||||
}
|
||||
}
|
||||
+54
-54
@@ -23,64 +23,64 @@ 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 EntityType {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
BOAT(protocolVersion -> {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
switch (protocolVersion) {
|
||||
default:
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
case MINECRAFT_1_8:
|
||||
case MINECRAFT_1_9:
|
||||
case MINECRAFT_1_9_1:
|
||||
case MINECRAFT_1_9_2:
|
||||
case MINECRAFT_1_9_4:
|
||||
case MINECRAFT_1_10:
|
||||
case MINECRAFT_1_11:
|
||||
case MINECRAFT_1_11_1:
|
||||
case MINECRAFT_1_12:
|
||||
case MINECRAFT_1_12_1:
|
||||
case MINECRAFT_1_12_2:
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
case MINECRAFT_1_13_2:
|
||||
return 1;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
return 5;
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 6;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
case MINECRAFT_1_19_3:
|
||||
return 8;
|
||||
case MINECRAFT_1_19_4:
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
|
||||
return 5;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 6;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 7;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 8;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
|
||||
return 9;
|
||||
}
|
||||
return 10;
|
||||
}),
|
||||
MINECART(protocolVersion -> {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
return 10;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
|
||||
return 41;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return 42;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
|
||||
return 45;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
return 50;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
return 53;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return 54;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
|
||||
return 64;
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
|
||||
return 65;
|
||||
}
|
||||
return 69;
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
+4
@@ -52,6 +52,10 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
writeString(byteBuf, FastUuidSansHyphens.toString(uuid));
|
||||
}
|
||||
|
||||
// Cap the length of the username to 16 characters
|
||||
if (username.length() > 16) {
|
||||
username = username.substring(0, 16);
|
||||
}
|
||||
writeString(byteBuf, username);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString(of = {"entityId", "hand"})
|
||||
public final class AnimationPacket implements FallbackPacket {
|
||||
private int entityId = -1;
|
||||
private int hand = MAIN_HAND;
|
||||
private LegacyAnimationType type = LegacyAnimationType.SWING_ARM;
|
||||
|
||||
public static final int MAIN_HAND = 0;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
entityId = byteBuf.readInt();
|
||||
type = LegacyAnimationType.getById(byteBuf.readByte());
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
|
||||
// Only 1.9+ clients have an offhand
|
||||
hand = readVarInt(byteBuf);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public enum LegacyAnimationType {
|
||||
NO_ANIMATION,
|
||||
SWING_ARM,
|
||||
DAMAGE_ANIMATION,
|
||||
LEAVE_BED,
|
||||
EAT_FOOD,
|
||||
CRITICAL_EFFECT,
|
||||
MAGIC_CRITICAL_EFFECT,
|
||||
UNKNOWN(102),
|
||||
CROUCH(104),
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
UNCROUCH(105);
|
||||
|
||||
private final int id;
|
||||
|
||||
LegacyAnimationType(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
LegacyAnimationType() {
|
||||
this.id = ordinal();
|
||||
}
|
||||
|
||||
public static LegacyAnimationType getById(int id) {
|
||||
if (id >= 0 && id <= 7) {
|
||||
return LegacyAnimationType.values()[id];
|
||||
}
|
||||
switch (id) {
|
||||
case 102:
|
||||
return UNKNOWN;
|
||||
case 104:
|
||||
return CROUCH;
|
||||
case 105:
|
||||
return UNCROUCH;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown type with id: " + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ChunkDataPacket implements FallbackPacket {
|
||||
private int x, z;
|
||||
private int sectionX, sectionZ;
|
||||
|
||||
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
|
||||
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
|
||||
@@ -67,8 +67,8 @@ public final class ChunkDataPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(z);
|
||||
byteBuf.writeInt(sectionX);
|
||||
byteBuf.writeInt(sectionZ);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
|
||||
@@ -113,13 +113,13 @@ public final class ChunkDataPacket implements FallbackPacket {
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
writeByteArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
} else {
|
||||
byteBuf.writeInt(0); // compressed size
|
||||
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
|
||||
}
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
|
||||
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
|
||||
writeByteArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
writeVarInt(byteBuf, 0); // 1.15 - 1.17.1
|
||||
} else {
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class EntityAnimationPacket implements FallbackPacket {
|
||||
private int entityId;
|
||||
private Type type;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
writeVarInt(byteBuf, entityId);
|
||||
byteBuf.writeByte(type.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
SWING_MAIN_ARM,
|
||||
HURT,
|
||||
WAKE_UP,
|
||||
// 1.9+?
|
||||
SWING_OFF_HAND, // Eat food on 1.7
|
||||
CRITICAL_HIT,
|
||||
MAGIC_CRITICAL_HIT
|
||||
// unknown (102), crouch (104), uncrouch(105) only exist on 1.7 and unused here
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -29,7 +29,6 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@@ -105,7 +104,7 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, OLD_CODEC);
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
@@ -123,7 +122,7 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, CODEC_1_16_2);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
|
||||
+3
-9
@@ -31,7 +31,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class MapDataPacket implements FallbackPacket {
|
||||
private int[] buffer;
|
||||
private byte[] buffer;
|
||||
private int x, y;
|
||||
|
||||
@Override
|
||||
@@ -43,7 +43,7 @@ public final class MapDataPacket implements FallbackPacket {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
byteBuf.writeByte(x);
|
||||
byteBuf.writeByte(y);
|
||||
writeArray(byteBuf, buffer);
|
||||
byteBuf.writeBytes(buffer);
|
||||
} else {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
|
||||
@@ -68,13 +68,7 @@ public final class MapDataPacket implements FallbackPacket {
|
||||
byteBuf.writeByte(y);
|
||||
|
||||
writeVarInt(byteBuf, buffer.length);
|
||||
writeArray(byteBuf, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeArray(final ByteBuf byteBuf, final int @NotNull [] array) {
|
||||
for (final int i : array) {
|
||||
byteBuf.writeByte(i);
|
||||
byteBuf.writeBytes(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerInfoPacket implements FallbackPacket {
|
||||
private String username;
|
||||
private UUID uuid;
|
||||
private int gamemode;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
writeString(byteBuf, username);
|
||||
byteBuf.writeBoolean(true); // online
|
||||
byteBuf.writeShort(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// https://wiki.vg/Protocol#player-info:player-actions
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) >= 0) {
|
||||
final EnumSet<Action> actions = EnumSet.noneOf(Action.class);
|
||||
actions.add(Action.ADD_PLAYER);
|
||||
actions.add(Action.UPDATE_LISTED);
|
||||
actions.add(Action.UPDATE_GAMEMODE);
|
||||
writeEnumSet(byteBuf, actions, Action.class);
|
||||
|
||||
writeVarInt(byteBuf, 1); // size
|
||||
writeUUID(byteBuf, uuid);
|
||||
writeString(byteBuf, username);
|
||||
writeVarInt(byteBuf, 0); // properties
|
||||
|
||||
byteBuf.writeBoolean(true); // update listed
|
||||
writeVarInt(byteBuf, gamemode);
|
||||
return;
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, 0); // ADD_PLAYER
|
||||
writeVarInt(byteBuf, 1);
|
||||
writeUUID(byteBuf, uuid);
|
||||
writeString(byteBuf, username);
|
||||
writeVarInt(byteBuf, 0);
|
||||
writeVarInt(byteBuf, gamemode);
|
||||
writeVarInt(byteBuf, 60);
|
||||
byteBuf.writeBoolean(false);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
ADD_PLAYER,
|
||||
INITIALIZE_CHAT,
|
||||
UPDATE_GAMEMODE,
|
||||
UPDATE_LISTED,
|
||||
UPDATE_LATENCY,
|
||||
UPDATE_DISPLAY_NAME
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@@ -43,7 +45,8 @@ public final class PlayerInputPacket implements FallbackPacket {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
sideways = byteBuf.readFloat();
|
||||
forward = byteBuf.readFloat();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
jump = byteBuf.readBoolean();
|
||||
unmount = byteBuf.readBoolean();
|
||||
} else {
|
||||
|
||||
+3
-3
@@ -18,12 +18,12 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readExtendedForgeShort;
|
||||
@@ -50,12 +50,12 @@ public final class PluginMessagePacket implements FallbackPacket {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
length = byteBuf.readableBytes();
|
||||
if (length > Short.MAX_VALUE) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
} else {
|
||||
length = readExtendedForgeShort(byteBuf);
|
||||
if (length > FORGE_MAX_ARRAY_LENGTH) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-10
@@ -23,10 +23,10 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.IntBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@@ -36,16 +36,13 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
private int slot, count, itemId;
|
||||
private int windowId, slot, count;
|
||||
private ItemType itemType;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(0); // windowId
|
||||
byteBuf.writeByte(windowId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
@@ -62,9 +59,9 @@ public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
byteBuf.writeShort(itemType.getId(protocolVersion));
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
writeVarInt(byteBuf, itemType.getId(protocolVersion));
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
@@ -86,7 +83,7 @@ public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else { // 1.20.5
|
||||
} else { // 1.20.5+
|
||||
// TODO: find a way to improve this
|
||||
// component
|
||||
writeVarInt(byteBuf, 1); // component count to add
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@@ -40,7 +41,7 @@ public final class SetExperiencePacket implements FallbackPacket {
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeFloat(experienceBar);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(level);
|
||||
byteBuf.writeShort(totalExperience);
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@@ -39,7 +40,7 @@ public final class SetPassengersPacket implements FallbackPacket {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
// You can find this in the EntityAttach packet,
|
||||
// which was later replaced by SetPassengers in 1.9+
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) < 0) {
|
||||
byteBuf.writeInt(passengerId);
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeByte(0); // leash
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
private int teleportId;
|
||||
private int teleportId, relativeMask;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
@@ -46,7 +46,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
byteBuf.writeDouble(z);
|
||||
byteBuf.writeFloat(yaw);
|
||||
byteBuf.writeFloat(pitch);
|
||||
byteBuf.writeBoolean(onGround);
|
||||
byteBuf.writeByte(relativeMask);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
|
||||
writeVarInt(byteBuf, teleportId);
|
||||
@@ -63,7 +63,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
y = byteBuf.readDouble(); // Account for 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
|
||||
+4
-4
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -26,6 +25,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -83,18 +83,18 @@ public final class SystemChatPacket implements FallbackPacket {
|
||||
|| saltLong == 0L) && signatureBytes.length == 0) {
|
||||
unsigned = true;
|
||||
} else {
|
||||
throw new CorruptedFrameException("Invalid signature");
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
final boolean signedPreview = byteBuf.readBoolean();
|
||||
if (signedPreview && unsigned) {
|
||||
throw new CorruptedFrameException("Signature missing");
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
final int size = readVarInt(byteBuf);
|
||||
if (size < 0 || size > 5) {
|
||||
throw new CorruptedFrameException("Invalid previous messages");
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TeleportEntityPacket implements FallbackPacket {
|
||||
private int entityId;
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
writeVarInt(byteBuf, entityId);
|
||||
} else {
|
||||
byteBuf.writeInt(entityId);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
|
||||
byteBuf.writeDouble(x);
|
||||
byteBuf.writeDouble(y);
|
||||
byteBuf.writeDouble(z);
|
||||
} else {
|
||||
byteBuf.writeInt((int) (x * 32D));
|
||||
byteBuf.writeInt((int) (y * 32D));
|
||||
byteBuf.writeInt((int) (z * 32D));
|
||||
}
|
||||
|
||||
byteBuf.writeByte(0); // pitch or yaw
|
||||
byteBuf.writeByte(0); // yaw or pitch
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeBoolean(onGround);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -54,7 +54,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
byteBuf.writeShort(block.getLegacyBlockState());
|
||||
final int blockId = block.getType().getId(protocolVersion);
|
||||
final int blockId = block.getBlockType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
writeVarInt(byteBuf, blockId);
|
||||
} else {
|
||||
@@ -82,7 +82,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
final int shiftedBlockId = block.getBlockType().getId(protocolVersion) << 12;
|
||||
final long positionIdValue = shiftedBlockId | block.getBlockState();
|
||||
writeVarLong(byteBuf, positionIdValue);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class VehicleMovePacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeDouble(x);
|
||||
byteBuf.writeDouble(y);
|
||||
byteBuf.writeDouble(z);
|
||||
byteBuf.writeFloat(yaw);
|
||||
byteBuf.writeFloat(pitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
pitch = byteBuf.readFloat();
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.ratelimit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Ticker;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
// Idea taken from Velocity
|
||||
@Setter
|
||||
@RequiredArgsConstructor
|
||||
public final class CaffeineCacheRatelimiter implements Ratelimiter<InetAddress> {
|
||||
private Cache<InetAddress, Long> expiringCache;
|
||||
private long timeout;
|
||||
|
||||
public CaffeineCacheRatelimiter(final long timeout, final @NotNull TimeUnit unit) {
|
||||
this.expiringCache = Caffeine.newBuilder()
|
||||
.ticker(Ticker.systemTicker())
|
||||
.expireAfterWrite(timeout, unit)
|
||||
.build();
|
||||
this.timeout = unit.toNanos(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
final long expectedNewTimestamp = System.nanoTime() + timeout;
|
||||
final long last = expiringCache.get(inetAddress, result -> expectedNewTimestamp);
|
||||
return expectedNewTimestamp == last;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.ratelimit;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
// Idea taken from Velocity
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class NoopCacheRatelimiter implements Ratelimiter<InetAddress> {
|
||||
public static final NoopCacheRatelimiter INSTANCE = new NoopCacheRatelimiter();
|
||||
|
||||
@Override
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Accept all connections
|
||||
return true;
|
||||
}
|
||||
}
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
protected final String username;
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected void checkState(final boolean state, final String failReason) {
|
||||
// Fails the verification if the condition is not met
|
||||
if (!state) {
|
||||
user.fail(failReason);
|
||||
}
|
||||
}
|
||||
|
||||
protected final void finishVerification() {
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getInetAddress(), user.getOfflineUuid(), user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
|
||||
username, user.getOfflineUuid(), user, user.getLoginTimer().delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Send the transfer packet to the player (and close the channel if on Java Edition)
|
||||
if (user.isGeyser()) {
|
||||
user.write(transferToOrigin);
|
||||
// Make sure we cannot receive any more packets from the player
|
||||
user.getPipeline().remove(FallbackPacketDecoder.class);
|
||||
user.getPipeline().remove(FallbackPacketEncoder.class);
|
||||
} else {
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
}
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", username)
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void checkClientInformation(final @NotNull ClientInformationPacket clientSettings) {
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientSettings.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
checkState((clientSettings.getSkinParts() & 0x80) == 0, "sent unused bit flag");
|
||||
|
||||
// Mark the ClientSettings packet as received
|
||||
user.setReceivedClientSettings(true);
|
||||
}
|
||||
|
||||
protected final void checkPluginMessage(final @NotNull PluginMessagePacket pluginMessage) {
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!user.isReceivedPluginMessage(), "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkState(usingLegacyChannel || v1_13, "invalid brand channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
// Mark the PluginMessage packet as received
|
||||
user.setReceivedPluginMessage(true);
|
||||
}
|
||||
|
||||
protected final void validateClientBrand(final byte @NotNull [] data) {
|
||||
// Check if the client brand is too short. It has to have at least 2 bytes.
|
||||
checkState(data.length > 1, "client brand is too short");
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand contains too much data: " + data.length);
|
||||
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
|
||||
String brand = new String(data, StandardCharsets.UTF_8);
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
brand = brand.substring(1);
|
||||
}
|
||||
// Check for illegal client brands
|
||||
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
|
||||
}
|
||||
|
||||
protected final void validateClientLocale(final @NotNull String locale) {
|
||||
// Check the client locale by performing a simple regex check
|
||||
// that disallows non-ascii characters by default.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SpawnEntityPacket} and {@link SetPassengersPacket} packets are sent to the client,
|
||||
* therefore, making the player enter a boat.
|
||||
* <br>
|
||||
* The boat will be spawned at y 1337.
|
||||
* <br>
|
||||
* After 3 {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets,
|
||||
* the boat is removed.
|
||||
* <br>
|
||||
* Now we can check if the player sends the correct position after the entity is removed.
|
||||
* Please note that we don't tell the client to exit the vehicle, therefore, possibly confusing some bots.
|
||||
* Removing the boat results in the player exiting the vehicle on vanilla clients.
|
||||
* <br>
|
||||
* See more: {@link FallbackVehicleSessionHandler}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackVehicleSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackVehicleSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Send the necessary packets to mount the player on the vehicle
|
||||
user.delayedWrite(spawnEntity);
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private int paddlePackets, inputPackets, positionPackets, rotationPackets;
|
||||
private boolean expectMovement;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Either send the player to the CAPTCHA or finish the verification.
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Send the player to the CAPTCHA handler
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
// We can abuse the entity remove mechanic and check for position packets when the entity dies
|
||||
private void handleMovement(double y) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
y -= 1.62f; // Account for 1.7 bounding box
|
||||
}
|
||||
// Check the Y position of the player
|
||||
checkState(y <= IN_AIR_Y_POSITION, "invalid y position");
|
||||
// Mark this check as successful if the player sent a few position packets
|
||||
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
|
||||
markSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRotation() {
|
||||
// Once the player sent enough packets, go to the next stage
|
||||
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
|
||||
if (paddlePackets > minimumPackets
|
||||
&& inputPackets > minimumPackets
|
||||
&& rotationPackets > minimumPackets) {
|
||||
// Remove the entity
|
||||
// The next y coordinate the player will send is going
|
||||
// to be the vehicle spawn position (y 64 in this case).
|
||||
user.write(removeEntities);
|
||||
// Listen for next movement packet(s)
|
||||
expectMovement = true;
|
||||
}
|
||||
rotationPackets++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof SetPlayerPositionRotationPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
|
||||
handleMovement(posRot.getY());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getY());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerRotationPacket) {
|
||||
if (!expectMovement && !user.isGeyser()) {
|
||||
handleRotation();
|
||||
}
|
||||
} else if (packet instanceof PaddleBoatPacket) {
|
||||
paddlePackets++;
|
||||
} else if (packet instanceof PlayerInputPacket && !expectMovement) {
|
||||
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
|
||||
|
||||
// Check if the player is sending invalid vehicle speed values
|
||||
final float forward = Math.abs(playerInput.getForward());
|
||||
final float sideways = Math.abs(playerInput.getSideways());
|
||||
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
|
||||
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
|
||||
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
|
||||
|
||||
// Only mark this packet as correct if the player is not moving the vehicle
|
||||
if (playerInput.isJump() || playerInput.isUnmount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
|
||||
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
|
||||
if (user.isGeyser()) {
|
||||
handleRotation();
|
||||
}
|
||||
|
||||
// 1.8 and below do not have PaddleBoat packets,
|
||||
// so we simply exempt them from the PaddleBoat check.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
paddlePackets++;
|
||||
}
|
||||
inputPackets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-26
@@ -15,9 +15,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
@@ -25,41 +24,31 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.MapCaptchaInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetContainerSlotPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionRotationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SystemChatPacket;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SetContainerSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
|
||||
* <br>
|
||||
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we wait for the player to enter the {@link FallbackCAPTCHASessionHandler#answer} in chat.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
|
||||
public final class FallbackCaptchaHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackCAPTCHASessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackCaptchaHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Disconnect the player if there is no CAPTCHA available at the moment
|
||||
if (!CaptchaPreparer.isCaptchaAvailable()) {
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
throw new CorruptedFrameException("No captcha available");
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// If the player is on Java, set the 5th slot (ID 4) in the player's hotbar to the map
|
||||
// If the player is on Bedrock, set the 1st slot (ID 0) in the player's hotbar to the map
|
||||
user.delayedWrite(new SetContainerSlotPacket(user.isGeyser() ? 36 : 40, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetContainerSlotPacket.MAP_NBT));
|
||||
final int slotId = user.isGeyser() ? 36 : 40;
|
||||
user.delayedWrite(new SetContainerSlotPacket(0, slotId, 1, ItemType.FILLED_MAP, MAP_ITEM_NBT));
|
||||
// Send random captcha to the player
|
||||
final MapCaptchaInfo captcha = CaptchaPreparer.getRandomCaptcha();
|
||||
this.answer = captcha.getAnswer().toLowerCase();
|
||||
@@ -67,7 +56,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
|
||||
user.delayedWrite(user.isGeyser() ? NO_MOVE_ABILITIES_BEDROCK : NO_MOVE_ABILITIES);
|
||||
// Make sure the player knows that they have to enter the code in chat
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
@@ -100,9 +89,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
if (Sonar.get().getConfig().getVerification().getGamemode().isSurvivalOrAdventure()) {
|
||||
final long difference = maxDuration - user.getLoginTimer().delay();
|
||||
final int index = (int) (difference / 1000D);
|
||||
// Make sure we can actually safely get and send the packet
|
||||
// Make sure we can safely get and send the packet
|
||||
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
|
||||
// Send the countdown using the experience bar
|
||||
user.write(xpCountdown[index]);
|
||||
}
|
||||
lastCountdownIndex = index;
|
||||
+84
-79
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -28,17 +28,19 @@ import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Game
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackGravitySessionHandler extends FallbackSessionHandler {
|
||||
public final class FallbackGravityHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackGravitySessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackGravityHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull FallbackPreJoinHandler preJoinHandler) {
|
||||
super(user);
|
||||
|
||||
this.preJoinHandler = preJoinHandler;
|
||||
// Bedrock users start falling immediately
|
||||
this.canFall = user.isGeyser();
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
this.enableGravityCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.enableCollisionsCheck = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.enableCollisionsCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
|
||||
// FIXME: 1.18.2-1.19.3 weird loading world issue
|
||||
// First, write the JoinGame packet to the buffer
|
||||
user.delayedWrite(joinGame);
|
||||
// Then, write the ClientAbilities packet to the buffer
|
||||
@@ -46,15 +48,17 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
if (Sonar.get().getConfig().getVerification().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Write the PositionLook packet to the buffer
|
||||
user.delayedWrite(spawnPosition);
|
||||
// Write the DefaultSpawnPosition packet to the buffer
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) >= 0) {
|
||||
user.delayedWrite(defaultSpawnPosition);
|
||||
}
|
||||
// Write the PlayerInfo packet to the buffer
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_16_4) >= 0) {
|
||||
user.delayedWrite(PLAYER_INFO);
|
||||
// Teleport the player to the position where we're starting to check them
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
user.delayedWrite(spawnPosition);
|
||||
user.delayedWrite(fallStartPosition);
|
||||
} else {
|
||||
// 1.7.2-1.7.10 clients do not have relative teleports
|
||||
user.delayedWrite(fallStartPositionLegacy);
|
||||
}
|
||||
// 1.20.3+ introduced game events
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
@@ -64,42 +68,37 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
// Teleport player into an empty world by sending an empty chunk packet
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Spawn the invisible platform below the player
|
||||
if (this.enableCollisionsCheck) {
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
if (enableCollisionsCheck) {
|
||||
final int index = RANDOM.nextInt(BLOCKS_PACKETS.length);
|
||||
blockHeight = POSSIBLE_BLOCK_TYPES[index].getBlockHeight();
|
||||
user.delayedWrite(BLOCKS_PACKETS[index]);
|
||||
}
|
||||
// Send all packets at once
|
||||
user.getChannel().flush();
|
||||
|
||||
// 1.8 and below don't have TeleportConfirm packets, which is why we're skipping that check.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
markTeleported();
|
||||
// Enable the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
}
|
||||
|
||||
private final FallbackPreJoinHandler preJoinHandler;
|
||||
private final boolean enableGravityCheck, enableCollisionsCheck;
|
||||
private boolean teleported, checkMovement;
|
||||
private double y, deltaY;
|
||||
private int movementTick;
|
||||
private boolean teleported, canFall, checkMovement;
|
||||
private double y, deltaY, blockHeight;
|
||||
private int movementTick, expectedTeleportId = FIRST_TELEPORT_ID;
|
||||
|
||||
private void markTeleported() {
|
||||
// Activate the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
|
||||
private void markSuccess(final boolean forceCAPTCHA) {
|
||||
private void markSuccess() {
|
||||
// Force-stop the movement checks
|
||||
teleported = false;
|
||||
|
||||
// Check if the player sent all necessary packets
|
||||
checkState(user.isReceivedClientSettings(), "didn't send client settings");
|
||||
// Don't check Geyser players for plugin messages
|
||||
if (!user.isGeyser()) {
|
||||
checkState(user.isReceivedPluginMessage(), "didn't send plugin message");
|
||||
// Exempt pre-1.20.2 since they've already passed that check in the configuration phase
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
preJoinHandler.checkClientInformation();
|
||||
}
|
||||
|
||||
// Send the player to the protocol check
|
||||
// Send the player to the next verification handler
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackProtocolSessionHandler(user, username, forceCAPTCHA));
|
||||
decoder.setListener(new FallbackProtocolHandler(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,38 +122,34 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
checkState(!teleported, "duplicate teleport confirm");
|
||||
// Check if the teleport ID matches the expected ID
|
||||
final int teleportId = teleportConfirm.getTeleportId();
|
||||
checkState(teleportId == TELEPORT_ID,
|
||||
"expected TP ID " + TELEPORT_ID + ", but got " + teleportId);
|
||||
checkState(teleportId == expectedTeleportId,
|
||||
"expected TP ID " + expectedTeleportId + ", but got " + teleportId);
|
||||
|
||||
markTeleported();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the play phase.
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
// The first teleport ID is not useful for us in this context, skip it
|
||||
if (expectedTeleportId == FIRST_TELEPORT_ID) {
|
||||
expectedTeleportId = SECOND_TELEPORT_ID;
|
||||
} else {
|
||||
// Enable the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
} else if (packet instanceof ClientInformationPacket
|
||||
|| packet instanceof PluginMessagePacket) {
|
||||
// Pass these packets back to the login handler
|
||||
preJoinHandler.handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMovement(final double x, final double y, final double z, final boolean isOnGround) {
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!checkMovement) {
|
||||
// No need to continue checking if the gravity and collision checks are disabled
|
||||
// Continue with the next stage of the verification
|
||||
if (!enableGravityCheck && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (x == SPAWN_X_POSITION && z == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
checkMovement = true;
|
||||
movementTick = 0;
|
||||
// Synchronize the Y coordinate
|
||||
this.y = dynamicSpawnYPosition;
|
||||
}
|
||||
// Synchronize the Y coordinate
|
||||
this.y = dynamicSpawnYPosition;
|
||||
checkMovement = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,81 +161,91 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
Sonar.get().getFallback().getLogger().info("{}: {}/{}/{}, ly={}/dy={}, {}",
|
||||
username, x, y, z, lastY, deltaY, isOnGround);
|
||||
Sonar.get().getLogger().info("{}: {}/{}/{} ly={}, dy={}, h={}, g={}",
|
||||
user.getUsername(), x, y, z, lastY, deltaY, blockHeight, isOnGround);
|
||||
}
|
||||
|
||||
// Ensure that the player's Y coordinate is above the collision platform
|
||||
checkState(y >= DEFAULT_Y_COLLIDE_POSITION,
|
||||
"fell through blocks: " + y + "/" + deltaY + "/" + movementTick);
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkState(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkState(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
checkState(Math.abs(Math.abs(x) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal x offset: " + x);
|
||||
checkState(Math.abs(Math.abs(z) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal z offset: " + z);
|
||||
|
||||
if (!isOnGround) {
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
// We have to account for this or the player will falsely fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Reset the movement tick as a safety measure
|
||||
movementTick = 0;
|
||||
checkState(movementTick == 0, "illegal y motion: " + movementTick);
|
||||
// 1.7 clients immediately start falling after this packet
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
movementTick++;
|
||||
}
|
||||
// We've received the first position packet; the player will now start falling
|
||||
canFall = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're actually expecting the player to fall at this point in time
|
||||
checkState(canFall, "unexpected y motion: " + deltaY);
|
||||
|
||||
// The movement tick should not be ignored when gravity check is disabled.
|
||||
movementTick++;
|
||||
|
||||
if (enableGravityCheck) {
|
||||
// Ensure that the player is above the collision platform
|
||||
checkState(y > DEFAULT_Y_COLLIDE_POSITION,
|
||||
"fell through blocks; y: " + y + " deltaY: " + deltaY + " tick: " + movementTick);
|
||||
|
||||
// Predict the player's current motion based on the last motion
|
||||
// https://minecraft.wiki/w/Entity#Motion_of_entities
|
||||
final double predicted = (lastDeltaY - 0.08) * 0.98f;
|
||||
final double difference = Math.abs(deltaY - predicted);
|
||||
|
||||
// Check if the difference between the predicted motion and
|
||||
// the actual motion is greater than our minimum threshold.
|
||||
// Check if the difference between the predicted and actual motion is too large
|
||||
if (difference > 1e-7) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("incorrect gravity; predicted: " + predicted + " deltaY: " + deltaY + " y: " + y);
|
||||
fail("incorrect gravity: " + predicted + "/ " + deltaY + "/" + y);
|
||||
}
|
||||
|
||||
// The player is obeying gravity, go on to the next stage if the collision check is disabled.
|
||||
if (movementTick == maxMovementTick && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
if (!enableCollisionsCheck && movementTick == maxMovementTick) {
|
||||
markSuccess();
|
||||
}
|
||||
}
|
||||
} else if (enableCollisionsCheck) {
|
||||
// Make sure the player has actually moved before reaching the platform
|
||||
if (movementTick < maxMovementTick) {
|
||||
if (enableGravityCheck && movementTick < maxMovementTick) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision tick; tick: " + movementTick);
|
||||
fail("illegal collision tick: " + movementTick + "/" + blockHeight);
|
||||
}
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockHeight) - y;
|
||||
// 1.7 sends the head position instead of the AABB minY
|
||||
// This little hack accounts for the offset of approximately 1.62
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f;
|
||||
}
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
if (collisionOffsetY < -0.03) {
|
||||
if (collisionOffsetY < -3e-2) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
user.setForceCaptcha(true);
|
||||
markSuccess();
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision; offset: " + collisionOffsetY + " tick: " + movementTick + " y: " + y);
|
||||
fail("illegal collision: " + collisionOffsetY + "/" + y + "/" + movementTick + "/" + blockHeight);
|
||||
}
|
||||
// The player has collided with the blocks, go on to the next stage
|
||||
markSuccess(false);
|
||||
markSuccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
-52
@@ -15,9 +15,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
@@ -29,39 +30,19 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformatio
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.KeepAlivePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* A {@link KeepAlivePacket} packet is sent to the client*
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#initialize18()}
|
||||
* </li>
|
||||
* <li>
|
||||
* After the KeepAlive packet is validated, the client enters the configuration phase.**
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, the session handler is set to the {@link FallbackGravitySessionHandler}.
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#markSuccess()}
|
||||
* </li>
|
||||
* <br>
|
||||
* * The KeepAlive check is skipped on 1.7, as KeepAlive packets don't exist during the LOGIN state.
|
||||
* <br>
|
||||
* ** The internals of the configuration phase were not mentioned.
|
||||
* Find out more about the client configuration and registry synchronization:
|
||||
* {@link #synchronizeClientRegistry()}, {@link #updateEncoderDecoderState(FallbackPacketRegistry)}
|
||||
*/
|
||||
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
public class FallbackPreJoinHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username) {
|
||||
super(user, username);
|
||||
public FallbackPreJoinHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
@@ -70,11 +51,12 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
user.getChannel().eventLoop().schedule(this::markSuccess, 100L, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
initialize18();
|
||||
sendKeepAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean receivedClientInfo, receivedClientBrand;
|
||||
private boolean acknowledgedLogin;
|
||||
private int expectedKeepAliveId;
|
||||
|
||||
@@ -83,31 +65,35 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
* is active and legitimate, thereby preventing bot connections that
|
||||
* could flood the server with login attempts and other unwanted traffic.
|
||||
*/
|
||||
private void initialize18() {
|
||||
private void sendKeepAlive() {
|
||||
// Send a KeepAlive packet with a random ID
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
user.write(new KeepAlivePacket(expectedKeepAliveId));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Make sure we can actually switch over to the next check
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
// Pass the player to the next verification handler
|
||||
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username);
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(gravitySessionHandler);
|
||||
}
|
||||
|
||||
private void markAcknowledged() {
|
||||
acknowledgedLogin = true;
|
||||
|
||||
// Update state, so we're able to send/receive configuration packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
synchronizeClientRegistry();
|
||||
// Write the FinishConfiguration packet to the buffer
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Set decoder state to actually catch all packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
}
|
||||
|
||||
protected final void checkClientInformation() {
|
||||
checkState(receivedClientInfo, "didn't send client settings");
|
||||
// Don't check Geyser players for plugin messages as they don't have them
|
||||
if (!user.isGeyser()) {
|
||||
checkState(receivedClientBrand, "didn't send plugin message");
|
||||
}
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next verification handler
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackGravityHandler(user, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,8 +111,7 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
// while the player is in the "Downloading terrain" screen.
|
||||
expectedKeepAliveId = 0;
|
||||
|
||||
// Spawn the player in the virtual world if the client does not need
|
||||
// any configuration (pre-1.20.2).
|
||||
// Immediately verify the player if they do not need any configuration (pre-1.20.2)
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
markSuccess();
|
||||
}
|
||||
@@ -135,21 +120,52 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
checkState(!acknowledgedLogin, "sent duplicate login ack");
|
||||
markAcknowledged();
|
||||
} else if (packet instanceof FinishConfigurationPacket) {
|
||||
checkClientInformation();
|
||||
// Update the encoder and decoder state because we're currently in the CONFIG state
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
markSuccess();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the configuration phase.
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof ClientInformationPacket) {
|
||||
final ClientInformationPacket clientInformation = (ClientInformationPacket) packet;
|
||||
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientInformation.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
// TODO: check if this causes issues with cosmetics in pvp clients
|
||||
checkState((clientInformation.getSkinParts() & 0x80) == 0,
|
||||
"sent unused bit flag: " + clientInformation.getSkinParts());
|
||||
|
||||
receivedClientInfo = true;
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
|
||||
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!receivedClientBrand, "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
checkState(usingLegacyChannel || user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0,
|
||||
"illegal PluginMessage channel: " + pluginMessage.getChannel());
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
receivedClientBrand = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
final var encoder = (FallbackPacketEncoder) user.getPipeline().get(FallbackPacketEncoder.class);
|
||||
final var encoder = user.getPipeline().get(FallbackPacketEncoder.class);
|
||||
// Update the packet registry state in the encoder and decoder pipelines
|
||||
decoder.updateRegistry(registry);
|
||||
encoder.updateRegistry(registry);
|
||||
@@ -168,4 +184,30 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClientBrand(final byte @NotNull [] data) {
|
||||
// Check if the client brand is too short. It has to have at least 2 bytes.
|
||||
checkState(data.length > 1, "client brand is too short");
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand contains too much data: " + data.length);
|
||||
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
|
||||
String brand = new String(data, StandardCharsets.UTF_8);
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
brand = brand.substring(1);
|
||||
}
|
||||
// Check for illegal client brands
|
||||
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
|
||||
}
|
||||
|
||||
private void validateClientLocale(final @NotNull String locale) {
|
||||
// Check the client locale by performing a simple regex check
|
||||
// that disallows non-ascii characters by default.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
|
||||
}
|
||||
}
|
||||
+65
-14
@@ -15,30 +15,32 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
package xyz.jonesdev.sonar.common.fallback.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.AnimationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EntityAnimationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetHeldItemPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
|
||||
|
||||
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.CACHED_HELD_ITEM_RESET;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.PLAYER_ENTITY_ID;
|
||||
|
||||
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username);
|
||||
public final class FallbackProtocolHandler extends FallbackVerificationHandler {
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
public FallbackProtocolHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Immediately send the player the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private boolean waitingSwingArm, waitingSlotConfirm;
|
||||
private short expectedTransactionId;
|
||||
private int currentClientSlotId, expectedSlotId = -1;
|
||||
|
||||
@@ -50,7 +52,7 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
*/
|
||||
private void sendTransaction() {
|
||||
// Send a Transaction (Ping) packet with a random ID
|
||||
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
@@ -63,6 +65,9 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
// Move the player's slot by 4 (slot limit divided by 2),
|
||||
// and then modulo it by the slot limit (8) to ensure that we don't send invalid slot IDs.
|
||||
expectedSlotId = (currentClientSlotId + 4) % 8;
|
||||
// Reset the player's slot to 4, so we don't get false positives if the client changes the slot
|
||||
// This doesn't *really* work for 1.7-1.8 clients, but it's better to still send a packet if they're de-sync
|
||||
user.delayedWrite(CACHED_HELD_ITEM_RESET);
|
||||
// Send two SetHeldItem packets with the same slot to check if the player responds with the correct slot.
|
||||
// By vanilla protocol, the client does not respond to duplicate SetHeldItem packets.
|
||||
// We can take advantage of this by sending two packets with the same content to check for a valid response.
|
||||
@@ -72,15 +77,26 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses EntityAnimation packets to check for the excepted Animation (SwingArm) from the client.
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Entity_Animation">Wiki.vg - EntityAnimation</a>
|
||||
* <a href="https://wiki.vg/Protocol#Swing_Arm">Wiki.vg - SwingArm</a>
|
||||
*/
|
||||
private void sendArmAnimation() {
|
||||
waitingSwingArm = true;
|
||||
user.write(new EntityAnimationPacket(PLAYER_ENTITY_ID, EntityAnimationPacket.Type.SWING_MAIN_ARM));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Either send the player to the vehicle check,
|
||||
// send the player to the CAPTCHA, or finish the verification.
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Pass the player to the next best verification handler
|
||||
if (Sonar.get().getConfig().getVerification().getVehicle().isEnabled()) {
|
||||
decoder.setListener(new FallbackVehicleSessionHandler(user, username, forceCAPTCHA));
|
||||
} else if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
|
||||
decoder.setListener(new FallbackVehicleHandler(user));
|
||||
} else if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCaptchaHandler(user));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
@@ -94,6 +110,8 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the window ID is valid
|
||||
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
@@ -108,9 +126,20 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
// https://wiki.vg/Bedrock_Protocol#Player_Hotbar
|
||||
if (user.isGeyser()) {
|
||||
markSuccess();
|
||||
} else if (waitingSlotConfirm) {
|
||||
waitingSlotConfirm = false;
|
||||
expectedSlotId = -1;
|
||||
// The player did not send duplicate packets, so they pass this check
|
||||
if (user.isGeyser()) {
|
||||
markSuccess();
|
||||
} else {
|
||||
sendArmAnimation();
|
||||
}
|
||||
} else {
|
||||
sendSetHeldItem();
|
||||
}
|
||||
|
||||
expectedTransactionId = 0;
|
||||
} else if (packet instanceof SetHeldItemPacket) {
|
||||
final SetHeldItemPacket heldItemPacket = (SetHeldItemPacket) packet;
|
||||
|
||||
@@ -125,11 +154,33 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
|
||||
if (expectedSlotId != -1
|
||||
// Check if the slot ID matches the expected slot ID
|
||||
// This can false flag if a player spams these packets, which is why we don't fail for this
|
||||
&& slotId == expectedSlotId) {
|
||||
markSuccess();
|
||||
&& slotId == expectedSlotId
|
||||
// Make sure we actually want to send a transaction at this point in time
|
||||
&& !waitingSlotConfirm) {
|
||||
sendTransaction();
|
||||
waitingSlotConfirm = true;
|
||||
}
|
||||
|
||||
currentClientSlotId = slotId;
|
||||
} else if (packet instanceof AnimationPacket) {
|
||||
// Make sure we are awaiting an AnimationPacket packet
|
||||
if (waitingSwingArm) {
|
||||
final AnimationPacket animationPacket = (AnimationPacket) packet;
|
||||
checkState(animationPacket.getHand() == AnimationPacket.MAIN_HAND,
|
||||
"invalid hand " + animationPacket.getHand());
|
||||
// Check that the 1.7 client is responding to the correct entity id and animation type.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
// Check entity id is player itself and if the player is sending the correct animation type
|
||||
if (animationPacket.getEntityId() == PLAYER_ENTITY_ID
|
||||
&& animationPacket.getType() == AnimationPacket.LegacyAnimationType.SWING_ARM) {
|
||||
markSuccess();
|
||||
waitingSwingArm = false;
|
||||
}
|
||||
} else {
|
||||
markSuccess();
|
||||
waitingSwingArm = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.verification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVehicleHandler extends FallbackVerificationHandler {
|
||||
|
||||
public FallbackVehicleHandler(final @NotNull FallbackUser user) {
|
||||
super(user);
|
||||
|
||||
// Send the necessary packets to mount the player on the boat vehicle
|
||||
user.delayedWrite(spawnBoatEntity);
|
||||
user.delayedWrite(setBoatPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private int expectedTransactionId;
|
||||
private int paddlePackets, inputPackets, positionPackets, rotationPackets, vehicleMovePackets;
|
||||
private boolean expectMovement, inVoid, inMinecart, waitingSpawnMinecart, waitingRemoveMinecart;
|
||||
private double minecartMotion, minecartY = IN_AIR_Y_POSITION;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Either send the player to the CAPTCHA or finish the verification.
|
||||
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Send the player to the CAPTCHA handler
|
||||
decoder.setListener(new FallbackCaptchaHandler(user));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
// We can abuse the entity remove mechanic and check for position packets when the entity dies
|
||||
private void handleMovement(double y, final boolean onGround) {
|
||||
// The player cannot be on the ground if the Y position is less than 0
|
||||
checkState(!onGround || !inVoid || waitingRemoveMinecart, "invalid ground state: " + y);
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
y -= 1.62f; // Account for 1.7 bounding box
|
||||
}
|
||||
// Check the Y position of the player
|
||||
final double minimumY = inVoid ? IN_VOID_Y_POSITION : IN_AIR_Y_POSITION;
|
||||
checkState(y <= minimumY, "illegal y position: " + y + "/" + minimumY);
|
||||
// Mark this check as successful if the player sent a few position packets
|
||||
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
|
||||
if (user.isGeyser() || inMinecart || inVoid) {
|
||||
markSuccess();
|
||||
} else {
|
||||
spawnMinecart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRotation() {
|
||||
// Once the player sent enough packets, go to the next stage
|
||||
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
|
||||
if (paddlePackets > minimumPackets
|
||||
&& inputPackets > minimumPackets
|
||||
&& rotationPackets > minimumPackets
|
||||
&& vehicleMovePackets > minimumPackets) {
|
||||
// If the player is riding a Minecart, teleport the entity to < -100
|
||||
// to see how the player reacts to the invalid position of the entity.
|
||||
if (inMinecart) {
|
||||
user.delayedWrite(teleportMinecart);
|
||||
// We're using transactions to avoid false positives with lag
|
||||
waitingRemoveMinecart = true;
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
|
||||
user.getChannel().flush();
|
||||
} else {
|
||||
// If the player is riding a boat, remove the entity
|
||||
// The next Y coordinate the player will send is going
|
||||
// to be the vehicle spawn position (64 in this case).
|
||||
user.write(removeBoat);
|
||||
}
|
||||
// Listen for next movement packet(s)
|
||||
expectMovement = true;
|
||||
}
|
||||
rotationPackets++;
|
||||
}
|
||||
|
||||
private void spawnMinecart() {
|
||||
expectMovement = false;
|
||||
paddlePackets = inputPackets = positionPackets = rotationPackets = 0;
|
||||
// Use transaction packets to confirm that the entity has spawned
|
||||
waitingSpawnMinecart = true;
|
||||
minecartMotion = 0.03999999910593033D * 2 * -1;
|
||||
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
|
||||
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
|
||||
user.delayedWrite(spawnMinecartEntity);
|
||||
user.delayedWrite(setMinecartPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the window ID is valid
|
||||
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
checkState(expectedTransactionId == transaction.getTransactionId(),
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transaction.getTransactionId());
|
||||
|
||||
// We're "lag compensating" the Minecart to avoid false positives
|
||||
if (waitingSpawnMinecart) {
|
||||
waitingSpawnMinecart = false;
|
||||
inMinecart = true;
|
||||
} else if (waitingRemoveMinecart) {
|
||||
waitingRemoveMinecart = inMinecart = false;
|
||||
inVoid = true;
|
||||
positionPackets = 0;
|
||||
}
|
||||
|
||||
expectedTransactionId = 0;
|
||||
} else if (packet instanceof SetPlayerPositionRotationPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
|
||||
handleMovement(posRot.getY(), posRot.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
if (expectMovement) {
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getY(), position.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerRotationPacket) {
|
||||
if (!expectMovement && !user.isGeyser()) {
|
||||
handleRotation();
|
||||
}
|
||||
} else if (packet instanceof PaddleBoatPacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected PaddleBoatPacket)");
|
||||
paddlePackets++;
|
||||
} else if (packet instanceof VehicleMovePacket) {
|
||||
checkState(!inMinecart, "invalid packet order (unexpected VehicleMovePacket)");
|
||||
final VehicleMovePacket vehicleMove = (VehicleMovePacket) packet;
|
||||
// Check the Y position of the vehicle
|
||||
checkState(vehicleMove.getY() <= IN_AIR_Y_POSITION, "invalid y position: " + vehicleMove.getY());
|
||||
if (!user.isGeyser()) {
|
||||
// Predict the Y motion of the minecart
|
||||
final double lastMinecartMotion = minecartMotion;
|
||||
final double lastMinecartY = minecartY;
|
||||
minecartY = vehicleMove.getY();
|
||||
minecartMotion = minecartY - lastMinecartY;
|
||||
final double predicted = lastMinecartMotion - 0.03999999910593033D;
|
||||
final double difference = Math.abs(minecartMotion - predicted);
|
||||
// Check if the difference between the predicted and actual motion is too large
|
||||
checkState(difference < 1e-7,
|
||||
"invalid vehicle gravity: " + predicted + "/" + minecartMotion + "/" + difference);
|
||||
}
|
||||
vehicleMovePackets++;
|
||||
} else if (packet instanceof PlayerInputPacket) {
|
||||
if (!expectMovement) {
|
||||
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
|
||||
|
||||
// Check if the player is sending invalid vehicle speed values
|
||||
final float forward = Math.abs(playerInput.getForward());
|
||||
final float sideways = Math.abs(playerInput.getSideways());
|
||||
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
|
||||
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
|
||||
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
|
||||
|
||||
// Only mark this packet as correct if the player is not moving the vehicle
|
||||
if (playerInput.isJump() || playerInput.isUnmount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
|
||||
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
|
||||
if (user.isGeyser()) {
|
||||
handleRotation();
|
||||
} else {
|
||||
checkState(rotationPackets >= inputPackets,
|
||||
"illegal packet order; r/i " + rotationPackets + "/" + inputPackets);
|
||||
}
|
||||
|
||||
// 1.8 and below do not have PaddleBoat packets, so we simply exempt them from the PaddleBoat check.
|
||||
// Clients don't send PaddleBoat & VehicleMovePacket packets while riding minecarts.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0 || inMinecart) {
|
||||
paddlePackets++;
|
||||
vehicleMovePackets++;
|
||||
} else {
|
||||
checkState(paddlePackets >= inputPackets,
|
||||
"illegal packet order; i/p " + inputPackets + "/" + paddlePackets);
|
||||
checkState(vehicleMovePackets >= inputPackets,
|
||||
"illegal packet order; i/v " + inputPackets + "/" + vehicleMovePackets);
|
||||
}
|
||||
inputPackets++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.verification;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
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.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_5;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected final void finishVerification() {
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getFingerprint(), user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(user, user.getLoginTimer()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Send the transfer packet to the player (and close the channel if on Java Edition)
|
||||
if (user.isGeyser()) {
|
||||
user.write(transferToOrigin);
|
||||
// Make sure we cannot receive any more packets from the player
|
||||
user.getPipeline().remove(FallbackPacketDecoder.class);
|
||||
user.getPipeline().remove(FallbackPacketEncoder.class);
|
||||
user.getPipeline().remove(FallbackVarInt21FrameDecoder.class);
|
||||
user.getPipeline().remove(FallbackVarIntLengthEncoder.class);
|
||||
} else {
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
}
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void fail(final @NotNull String reason) {
|
||||
GlobalSonarStatistics.totalFailedVerifications++;
|
||||
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol()))
|
||||
.replace("<reason>", reason));
|
||||
}
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(user, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method in the future
|
||||
blacklist: {
|
||||
final String hostAddress = user.getInetAddress().getHostAddress();
|
||||
final int score = Sonar.get().getFallback().getBlacklist().get(hostAddress, __ -> 0);
|
||||
final int newScore = score + 1;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(hostAddress, newScore);
|
||||
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The player hasn't been blacklisted yet, so skip this iteration
|
||||
if (newScore < limit) break blacklist;
|
||||
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(user));
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<username>", user.getUsername())
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw QuietDecoderException.INSTANCE;
|
||||
}
|
||||
|
||||
protected final void checkState(final boolean state, final String failReason) {
|
||||
// Fails the verification if the condition is not met
|
||||
if (!state) {
|
||||
fail(failReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.common.service;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
import xyz.jonesdev.sonar.common.statistics.BandwidthStatistics;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
@@ -43,21 +43,19 @@ public final class SonarServiceManager {
|
||||
|
||||
public void start() {
|
||||
VERBOSE.scheduleAtFixedRate(() -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
// Make sure to clean up the cached statistics since we don't want to display wrong values
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().observe();
|
||||
// Publish the action bar notifications
|
||||
Sonar.get().getActionBarNotificationHandler().handleNotification();
|
||||
}, 0L, 250L, TimeUnit.MILLISECONDS);
|
||||
|
||||
FALLBACK_QUEUE.scheduleAtFixedRate(() -> Sonar.get().getFallback().getQueue().poll(),
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
STATISTICS.scheduleAtFixedRate(CachedBandwidthStatistics::reset,
|
||||
STATISTICS.scheduleAtFixedRate(BandwidthStatistics::reset,
|
||||
0L, 1L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -19,13 +19,14 @@ package xyz.jonesdev.sonar.common.statistics;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import xyz.jonesdev.sonar.api.jvm.JVMProcessInformation;
|
||||
import xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler;
|
||||
|
||||
// TODO: maybe recode this?
|
||||
/**
|
||||
* This counts all incoming and outgoing traffic.
|
||||
*/
|
||||
@Getter
|
||||
public enum CachedBandwidthStatistics {
|
||||
public enum BandwidthStatistics {
|
||||
INCOMING,
|
||||
OUTGOING;
|
||||
|
||||
@@ -54,8 +55,8 @@ public enum CachedBandwidthStatistics {
|
||||
|
||||
private void cacheAndReset() {
|
||||
ttl += curr; // increment total
|
||||
cachedSecond = JVMProcessInformation.formatMemory(curr);
|
||||
cachedTtl = JVMProcessInformation.formatMemory(ttl);
|
||||
cachedSecond = SimpleProcessProfiler.formatMemory(curr);
|
||||
cachedTtl = SimpleProcessProfiler.formatMemory(ttl);
|
||||
curr = 0L; // reset current
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -80,42 +80,42 @@ public final class GlobalSonarStatistics implements SonarStatistics {
|
||||
|
||||
@Override
|
||||
public long getCurrentIncomingBandwidth() {
|
||||
return CachedBandwidthStatistics.INCOMING.getCurr();
|
||||
return BandwidthStatistics.INCOMING.getCurr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentOutgoingBandwidth() {
|
||||
return CachedBandwidthStatistics.OUTGOING.getCurr();
|
||||
return BandwidthStatistics.OUTGOING.getCurr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalIncomingBandwidth() {
|
||||
return CachedBandwidthStatistics.INCOMING.getTtl();
|
||||
return BandwidthStatistics.INCOMING.getTtl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalOutgoingBandwidth() {
|
||||
return CachedBandwidthStatistics.OUTGOING.getTtl();
|
||||
return BandwidthStatistics.OUTGOING.getTtl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentIncomingBandwidthFormatted() {
|
||||
return CachedBandwidthStatistics.INCOMING.getCachedSecond();
|
||||
return BandwidthStatistics.INCOMING.getCachedSecond();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentOutgoingBandwidthFormatted() {
|
||||
return CachedBandwidthStatistics.OUTGOING.getCachedSecond();
|
||||
return BandwidthStatistics.OUTGOING.getCachedSecond();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTotalIncomingBandwidthFormatted() {
|
||||
return CachedBandwidthStatistics.INCOMING.getCachedTtl();
|
||||
return BandwidthStatistics.INCOMING.getCachedTtl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTotalOutgoingBandwidthFormatted() {
|
||||
return CachedBandwidthStatistics.OUTGOING.getCachedTtl();
|
||||
return BandwidthStatistics.OUTGOING.getCachedTtl();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+20
-56
@@ -15,10 +15,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -27,9 +25,6 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "blacklist",
|
||||
arguments = {"add", "remove", "clear", "size"}
|
||||
@@ -49,38 +44,19 @@ public final class BlacklistCommand extends Subcommand {
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
try {
|
||||
// Try to resolve the IP address, so we can actually do something with it
|
||||
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
Sonar.get().getFallback().getBlacklist().put(inetAddress, (byte) 0);
|
||||
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSource().sendMessage(Component.text(
|
||||
"Unexpected error, check console.", NamedTextColor.RED));
|
||||
exception.printStackTrace(System.err);
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(rawAddress, 1337 /* arbitrarily high number */);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -94,29 +70,18 @@ public final class BlacklistCommand extends Subcommand {
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
try {
|
||||
// Try to resolve the IP address, so we can actually do something with it
|
||||
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
Sonar.get().getFallback().getBlacklist().invalidate(inetAddress);
|
||||
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSource().sendMessage(Component.text(
|
||||
"Unexpected error, check console.", NamedTextColor.RED));
|
||||
exception.printStackTrace(System.err);
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().invalidate(rawAddress);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -130,7 +95,6 @@ public final class BlacklistCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getFallback().getBlacklist().invalidateAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.clear"),
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
@@ -32,7 +32,7 @@ import java.lang.management.RuntimeMXBean;
|
||||
import java.util.List;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
|
||||
|
||||
// This command is only used to display helpful information to help
|
||||
// the Sonar contributors to fix issues more quickly
|
||||
+4
-4
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
@@ -33,15 +33,15 @@ public final class NotifyCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (Sonar.get().getNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getNotificationHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
if (Sonar.get().getChatNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getChatNotificationHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.unsubscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getNotificationHandler().subscribe(invocation.getSource().getUuid());
|
||||
Sonar.get().getChatNotificationHandler().subscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.subscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
+7
-7
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
@@ -26,10 +26,10 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
import xyz.jonesdev.sonar.common.statistics.BandwidthStatistics;
|
||||
|
||||
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.*;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "statistics",
|
||||
@@ -109,10 +109,10 @@ public final class StatisticsCommand extends Subcommand {
|
||||
case "network": {
|
||||
placeholders = new TagResolver.Single[]{
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("incoming-traffic", CachedBandwidthStatistics.INCOMING.getCachedSecond()),
|
||||
Placeholder.unparsed("outgoing-traffic", CachedBandwidthStatistics.OUTGOING.getCachedSecond()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", CachedBandwidthStatistics.INCOMING.getCachedTtl()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", CachedBandwidthStatistics.OUTGOING.getCachedTtl())
|
||||
Placeholder.unparsed("incoming-traffic", BandwidthStatistics.INCOMING.getCachedSecond()),
|
||||
Placeholder.unparsed("outgoing-traffic", BandwidthStatistics.OUTGOING.getCachedSecond()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", BandwidthStatistics.INCOMING.getCachedTtl()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", BandwidthStatistics.OUTGOING.getCachedTtl())
|
||||
};
|
||||
break;
|
||||
}
|
||||
+4
-4
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
@@ -34,8 +34,8 @@ public final class VerboseCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (Sonar.get().getVerboseHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getVerboseHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
if (Sonar.get().getActionBarNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getActionBarNotificationHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
// Reset ActionBar component when unsubscribing
|
||||
invocation.getSource().getAudience().sendActionBar(Component.empty());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
@@ -44,7 +44,7 @@ public final class VerboseCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getVerboseHandler().subscribe(invocation.getSource().getUuid());
|
||||
Sonar.get().getActionBarNotificationHandler().subscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.subscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
+24
-65
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
@@ -24,113 +24,73 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verified",
|
||||
arguments = {"add", "remove", "clear", "size", "history"}
|
||||
arguments = {"add", "remove", "clear", "size"}
|
||||
)
|
||||
public final class VerifiedCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified history <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
// Make sure the IP is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
|
||||
for (final UUID uuid : Sonar.get().getVerifiedPlayerController().getUUIDs(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history-entry"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("uuid", uuid.toString())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified remove <IP address>");
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
incorrectUsage(invocation.getSource(), "verified remove <IP address> <username>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
// An InetAddress starts with /, we need to account for this
|
||||
final String inetAddress = "/" + rawAddress;
|
||||
if (hostAddress == null) return;
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
|
||||
final String username = invocation.getRawArguments()[3];
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
|
||||
if (!Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getVerifiedPlayerController().remove(inetAddress);
|
||||
Sonar.get().getVerifiedPlayerController().remove(fingerprint);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
Placeholder.unparsed("ip", hostAddress),
|
||||
Placeholder.unparsed("username", username)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
incorrectUsage(invocation.getSource(), "verified add <IP address> <UUID/username>");
|
||||
incorrectUsage(invocation.getSource(), "verified add <IP address> <username>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
// An InetAddress starts with /, we need to account for this
|
||||
final String inetAddress = "/" + rawAddress;
|
||||
if (hostAddress == null) return;
|
||||
|
||||
// Try to parse the UUID (from the username, if needed)
|
||||
final String rawUUID = invocation.getRawArguments()[3];
|
||||
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
|
||||
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
|
||||
final String username = invocation.getRawArguments()[3];
|
||||
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
|
||||
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(fingerprint, System.currentTimeMillis()));
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
Placeholder.unparsed("ip", hostAddress),
|
||||
Placeholder.unparsed("username", username)));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -144,7 +104,6 @@ public final class VerifiedCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getVerifiedPlayerController().clearAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.clear"),
|
||||
@@ -22,7 +22,6 @@ import com.google.gson.JsonParser;
|
||||
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;
|
||||
@@ -37,24 +36,6 @@ import java.util.concurrent.Executors;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
@@ -66,17 +47,17 @@ 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/tag/{}", latestStableRelease);
|
||||
} 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");
|
||||
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");
|
||||
} else {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
Sonar.get().getLogger().info("You are currently using the latest stable release of Sonar!");
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.warn("Unable to retrieve version information: {}", throwable);
|
||||
Sonar.get().getLogger().warn("Unable to retrieve version information: {}", throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ public final class ComponentHolder {
|
||||
this.serializedComponent = GsonComponentSerializer.gson().serialize(component);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
|
||||
private BinaryTag serialize(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
@@ -107,21 +106,21 @@ public final class ComponentHolder {
|
||||
case 1://BinaryTagTypes.BYTE:
|
||||
final byte[] bytes = new byte[jsonArray.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
|
||||
bytes[i] = jsonArray.get(i).getAsNumber().byteValue();
|
||||
}
|
||||
|
||||
return ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
|
||||
case 3://BinaryTagTypes.INT:
|
||||
final int[] ints = new int[jsonArray.size()];
|
||||
for (int i = 0; i < ints.length; i++) {
|
||||
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
|
||||
ints[i] = jsonArray.get(i).getAsNumber().intValue();
|
||||
}
|
||||
|
||||
return IntArrayBinaryTag.intArrayBinaryTag(ints);
|
||||
case 4://BinaryTagTypes.LONG:
|
||||
final long[] longs = new long[jsonArray.size()];
|
||||
for (int i = 0; i < longs.length; i++) {
|
||||
longs[i] = (Long) jsonArray.get(i).getAsNumber();
|
||||
longs[i] = jsonArray.get(i).getAsNumber().longValue();
|
||||
}
|
||||
|
||||
return LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
|
||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user