mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326a1ecc07 | ||
|
|
6af5a9fc4b | ||
|
|
ac78d88721 | ||
|
|
75abebde7c | ||
|
|
28b1615b78 | ||
|
|
600275669b | ||
|
|
0c3761be11 | ||
|
|
b7d8ec74d0 | ||
|
|
96efa574e0 | ||
|
|
8db3d0257f | ||
|
|
106a44f78a | ||
|
|
a5872f1b6e | ||
|
|
e9c212b9e4 | ||
|
|
42da141f2b | ||
|
|
e0bc83f8ef | ||
|
|
59822ef541 | ||
|
|
38fbfff896 | ||
|
|
6725975cf1 | ||
|
|
d59fd4b431 | ||
|
|
a76ca2f91d | ||
|
|
30803ba6e4 | ||
|
|
194232804b | ||
|
|
19c97b26b6 | ||
|
|
d66719b3e0 | ||
|
|
9226908002 | ||
|
|
ac74f14dc0 | ||
|
|
8f11a53b38 |
No files matched your search
@@ -9,6 +9,7 @@
|
||||
|
||||
<!-- Badges & icons -->
|
||||
[](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
|
||||
[](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
|
||||
[](https://github.com/jonesdevelopment/sonar/releases)
|
||||
[](https://github.com/jonesdevelopment/sonar/issues)
|
||||
[](https://jonesdev.xyz/discord)
|
||||
|
||||
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
|
||||
|
||||
+16
-3
@@ -35,7 +35,7 @@ allprojects {
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.30")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
|
||||
val adventureVersion = "4.14.0"
|
||||
val adventureVersion = "4.15.0"
|
||||
|
||||
// adventure
|
||||
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
|
||||
@@ -46,7 +46,7 @@ allprojects {
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.101.Final") // netty
|
||||
compileOnly("io.netty:netty-all:4.1.104.Final") // netty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,16 +71,29 @@ tasks {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("Sonar.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
|
||||
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
exclude("net.kyori.adventure.audience.**")
|
||||
}
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/versions/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.9
|
||||
version=2.0.13
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -27,8 +27,10 @@ import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.dependencies.Dependency;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -48,8 +50,6 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
private final Verification verification = new Verification();
|
||||
@Getter
|
||||
private final Lockdown lockdown = new Lockdown();
|
||||
@Getter
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
@@ -98,12 +98,53 @@ public final class SonarConfiguration {
|
||||
private Timing timing;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Timing {
|
||||
ALWAYS, DURING_ATTACK, NEVER
|
||||
}
|
||||
|
||||
private boolean checkGravity;
|
||||
private final Map map = new Map();
|
||||
private final Gravity gravity = new Gravity();
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private Timing timing;
|
||||
private boolean randomizePositions;
|
||||
private boolean randomizeFontSize;
|
||||
private double distortionsFactorX;
|
||||
private double distortionsFactorY;
|
||||
private int randomLinesAmount;
|
||||
private int randomOvalsAmount;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
private String enterCodeActionBar;
|
||||
private List<String> fonts;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Gravity {
|
||||
private boolean enabled;
|
||||
private Gamemode gamemode;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
private Component youAreBeingChecked;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
// Keep this for backwards compatibility
|
||||
SPECTATOR(2);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
@@ -115,22 +156,7 @@ public final class SonarConfiguration {
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
|
||||
private Gamemode gamemode;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
SPECTATOR(3);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
|
||||
private int maxBrandLength;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
private int maxLoginPackets;
|
||||
private int maxPing;
|
||||
private int readTimeout;
|
||||
@@ -149,6 +175,7 @@ public final class SonarConfiguration {
|
||||
private Component alreadyQueued;
|
||||
private Component blacklisted;
|
||||
private Component protocolBlacklisted;
|
||||
private Component currentlyPreparing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -216,21 +243,7 @@ public final class SonarConfiguration {
|
||||
private String name;
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Lockdown {
|
||||
@Setter
|
||||
private boolean enabled;
|
||||
private boolean notifyAdmins;
|
||||
private boolean logAttempts;
|
||||
private Component disconnect;
|
||||
private String bypassPermission;
|
||||
private String activated;
|
||||
private String deactivated;
|
||||
private String notification;
|
||||
private String consoleLog;
|
||||
private int maximumAge;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -366,26 +379,9 @@ public final class SonarConfiguration {
|
||||
"Password for authenticating with the SQL database");
|
||||
database.password = generalConfig.getString("database.password", "");
|
||||
|
||||
// Lockdown
|
||||
generalConfig.getYaml().setComment("lockdown",
|
||||
"You can lock your server down using '/sonar lockdown' if, for example,"
|
||||
+ LINE_SEPARATOR + "bots are bypassing the verification or any other reason");
|
||||
generalConfig.getYaml().setComment("lockdown.enabled",
|
||||
"Should Sonar prevent all players from joining the server?");
|
||||
lockdown.enabled = generalConfig.getBoolean("lockdown.enabled", false);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.log-attempts",
|
||||
"Should Sonar log new login attempts during lockdown?");
|
||||
lockdown.logAttempts = generalConfig.getBoolean("lockdown.log-attempts", true);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.notify-admins",
|
||||
"Should Sonar notify admins when they join the server during lockdown?");
|
||||
lockdown.notifyAdmins = generalConfig.getBoolean("lockdown.notify-admins", true);
|
||||
|
||||
generalConfig.getYaml().setComment("lockdown.bypass-permission",
|
||||
"Which permission does a player need in order to bypass the lockdown mode?"
|
||||
+ LINE_SEPARATOR + "Players with this permission will also receive admin notifications when joining");
|
||||
lockdown.bypassPermission = generalConfig.getString("lockdown.bypass-permission", "sonar.lockdown.bypass");
|
||||
generalConfig.getYaml().setComment("database.maximum-age",
|
||||
"How many days should Sonar keep verified players in the database?");
|
||||
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age", 5), 1, 365);
|
||||
|
||||
// Queue
|
||||
generalConfig.getYaml().setComment("queue",
|
||||
@@ -413,17 +409,90 @@ public final class SonarConfiguration {
|
||||
+ LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance");
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.enabled",
|
||||
"Should Sonar check for valid client gravity? (Recommended)");
|
||||
verification.checkGravity = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks",
|
||||
"Maximum number of ticks the player has to fall in order to be allowed to hit the platform");
|
||||
verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8),
|
||||
2, 100);
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt(
|
||||
"verification.checks.gravity.max-movement-ticks", 8), 2, 100);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks",
|
||||
"Maximum number of ignored Y movement changes before a player fails verification");
|
||||
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
|
||||
128);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt(
|
||||
"verification.checks.gravity.max-ignored-ticks", 5), 1, 128);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity.gamemode",
|
||||
"The gamemode of the player during verification"
|
||||
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
|
||||
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
|
||||
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible");
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(
|
||||
generalConfig.getString("verification.checks.gravity.gamemode",
|
||||
Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha",
|
||||
"Make the player type a code from a virtual map in chat");
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.timing",
|
||||
"When should Sonar make the player solve a captcha?"
|
||||
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
|
||||
+ LINE_SEPARATOR + "- ALWAYS: New players will always receive a captcha"
|
||||
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only receive a captcha during an attack"
|
||||
+ LINE_SEPARATOR + "- NEVER: New players will never receive a captcha (Recommended)");
|
||||
verification.map.timing = Verification.Timing.valueOf(
|
||||
generalConfig.getString("verification.checks.map-captcha.timing",
|
||||
Verification.Timing.NEVER.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-position",
|
||||
"Should Sonar randomize the X and Y position of the captcha?");
|
||||
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.distortions-factor-x",
|
||||
"How much should Sonar distort characters (factor for randomization)?");
|
||||
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-x", 50), 0, 100) / 100D;
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.distortions-factor-y",
|
||||
"How much should Sonar distort characters (factor for randomization)?");
|
||||
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-y", 50), 0, 100) / 100D;
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-lines",
|
||||
"How many random lines behind the captcha should Sonar draw?");
|
||||
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines", 4);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-ovals",
|
||||
"How many random ovals behind the captcha should Sonar draw?");
|
||||
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals", 1);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-font-size",
|
||||
"Should Sonar randomize the size of the font used for rendering the captcha?");
|
||||
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size",
|
||||
true);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute",
|
||||
"How many answers should Sonar precompute (prepare)?"
|
||||
+ LINE_SEPARATOR + "This task happens asynchronously in the background;"
|
||||
+ LINE_SEPARATOR + "Players are able to join once one captcha has been prepared");
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
|
||||
"How long should Sonar wait until the player fails the captcha?");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-tries",
|
||||
"How many times must a player fail the captcha before failing the verification?");
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries", 3);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary",
|
||||
"Characters (letters and numbers) that are allowed to appear in the answer to the captcha");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789");
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.map-captcha.fonts",
|
||||
"Which font types should Sonar use for the map captcha codes?");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts",
|
||||
Arrays.asList(Font.DIALOG, Font.DIALOG_INPUT, Font.SERIF, Font.SANS_SERIF));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
@@ -452,16 +521,6 @@ public final class SonarConfiguration {
|
||||
"Maximum number of login packets the player has to send in order to be kicked");
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.gamemode",
|
||||
"The gamemode of the player during verification"
|
||||
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
|
||||
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
|
||||
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- SPECTATOR: all UI components are hidden (Recommended)");
|
||||
verification.gamemode = Verification.Gamemode.valueOf(
|
||||
generalConfig.getString("verification.gamemode", Verification.Gamemode.SPECTATOR.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.log-connections",
|
||||
"Should Sonar log new verification attempts?");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections", true);
|
||||
@@ -485,16 +544,18 @@ public final class SonarConfiguration {
|
||||
|
||||
generalConfig.getYaml().setComment("verification.whitelisted-protocols",
|
||||
"List of protocol IDs which are not checked by Sonar (verification bypass)"
|
||||
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
|
||||
+ LINE_SEPARATOR + "https://wiki.vg/Protocol_version_numbers"
|
||||
+ LINE_SEPARATOR + "For example, Minecraft 1.20 has the ID 763.");
|
||||
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
|
||||
+ LINE_SEPARATOR + "https://wiki.vg/Protocol_version_numbers"
|
||||
+ LINE_SEPARATOR + "For example, Minecraft 1.20 has the ID 763.");
|
||||
verification.whitelistedProtocols.clear();
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols", new ArrayList<>(0)));
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols",
|
||||
new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.blacklisted-protocols",
|
||||
"List of protocol IDs which are unable to join the server at all");
|
||||
verification.blacklistedProtocols.clear();
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols", new ArrayList<>(0)));
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols",
|
||||
new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("webhook",
|
||||
"Bot attack notifications can also be sent to your Discord server using webhooks");
|
||||
@@ -641,29 +702,6 @@ public final class SonarConfiguration {
|
||||
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission",
|
||||
"%prefix%<red>You do not have permission to execute this subcommand. <gray>(%permission%)"));
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown",
|
||||
"Translations for '/sonar lockdown'");
|
||||
messagesConfig.getYaml().setComment("lockdown.notification",
|
||||
"Message that is shown when an admin joins the server during lockdown");
|
||||
lockdown.notification = formatString(messagesConfig.getString("lockdown.notification",
|
||||
"%prefix%<green>Hey, the server is currently in lockdown mode. If you want to disable the lockdown mode,"
|
||||
+ " type <white>/sonar lockdown<green>."
|
||||
));
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown.console-log",
|
||||
"Message that is shown to console when a normal player tries joining the server during lockdown");
|
||||
lockdown.consoleLog = messagesConfig.getString("lockdown.console-log",
|
||||
"%player% (%ip%, %protocol%) tried to join during lockdown mode.");
|
||||
|
||||
messagesConfig.getYaml().setComment("lockdown.disconnect-message",
|
||||
"Message that is shown to a normal player when they try joining the server during lockdown");
|
||||
lockdown.disconnect = deserialize(fromList(messagesConfig.getStringList("lockdown.disconnect-message",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<red>The server is currently locked down, please try again later.",
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.main",
|
||||
"Translations for '/sonar'");
|
||||
messagesConfig.getYaml().setComment("commands.main.header",
|
||||
@@ -703,18 +741,6 @@ public final class SonarConfiguration {
|
||||
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish",
|
||||
"%prefix%<green>Successfully reloaded <gray>(%taken%ms)"));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.lockdown",
|
||||
"Translations for '/sonar lockdown'");
|
||||
messagesConfig.getYaml().setComment("commands.lockdown.enabled",
|
||||
"Message that is shown when a player enables server lockdown");
|
||||
lockdown.activated = formatString(messagesConfig.getString("commands.lockdown.enabled",
|
||||
"%prefix%The server is now in lockdown mode."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.lockdown.disabled",
|
||||
"Message that is shown when a player disables server lockdown");
|
||||
lockdown.deactivated = formatString(messagesConfig.getString("commands.lockdown.disabled",
|
||||
"%prefix%The server is no longer in lockdown mode."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose",
|
||||
"Translations for '/sonar verbose'");
|
||||
messagesConfig.getYaml().setComment("commands.verbose.subscribed",
|
||||
@@ -883,6 +909,36 @@ public final class SonarConfiguration {
|
||||
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful",
|
||||
"%name% has been verified successfully (%time%s!)."));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.welcome",
|
||||
"Message that is shown to the player when they are being checked for valid gravity");
|
||||
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome",
|
||||
"%prefix%<gray>Please wait a moment for the verification to finish...")));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.captcha.enter-code",
|
||||
"Message that is shown to the player when they have to enter the answer to the captcha");
|
||||
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code",
|
||||
"%prefix%<green>Please enter the code in chat that is displayed on the map.")));
|
||||
messagesConfig.getYaml().setComment("verification.captcha.action-bar",
|
||||
"Timer that is shown to the player when they have to enter the answer to the captcha"
|
||||
+ LINE_SEPARATOR + "(Set this to '' to disable the action bar message)");
|
||||
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar",
|
||||
"%prefix%<green>You have %time-left% seconds left to enter the code in chat"));
|
||||
messagesConfig.getYaml().setComment("verification.captcha.incorrect",
|
||||
"Message that is shown to the player when they enter the wrong answer in chat");
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect",
|
||||
"%prefix%<red>You have entered the wrong code. Please try again.")));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.currently-preparing",
|
||||
"Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet");
|
||||
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList(
|
||||
"verification.currently-preparing",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<yellow>Your anti-bot data has not been prepared yet.",
|
||||
"<gray>Please wait a few seconds before trying to verify again.",
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.too-many-players",
|
||||
"Disconnect message that is shown when too many players are verifying at the same time");
|
||||
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players",
|
||||
@@ -954,7 +1010,8 @@ public final class SonarConfiguration {
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.blacklisted-protocol",
|
||||
"Disconnect message that is shown when someone joins with a blacklisted version");
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol",
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification" +
|
||||
".blacklisted-protocol",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<red>You are using a version that is not allowed on our server.",
|
||||
|
||||
+45
@@ -25,6 +25,7 @@ import com.j256.ormlite.table.TableUtils;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.dependencies.DependencyLoader;
|
||||
@@ -32,6 +33,9 @@ import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
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.concurrent.ExecutorService;
|
||||
@@ -72,6 +76,9 @@ public final class VerifiedPlayerController {
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
|
||||
@@ -197,6 +204,44 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
public void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final long timestamp = Instant.now()
|
||||
.minus(maximumAge, ChronoUnit.DAYS)
|
||||
.getEpochSecond() * 1000L; // convert to ms
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
|
||||
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
@@ -67,7 +66,14 @@ public final class Fallback {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
val timing = Sonar.get().getConfig().getVerification().getTiming();
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
|
||||
}
|
||||
|
||||
public boolean shouldDoMapCaptcha() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
}
|
||||
|
||||
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
|
||||
@@ -69,5 +69,7 @@ public final class FallbackQueue {
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,15 @@ public final class FallbackRatelimiter {
|
||||
@Setter
|
||||
private ExpiringCache<InetAddress> expiringCache;
|
||||
|
||||
/**
|
||||
* We don't want to clean up the cache in the
|
||||
* {@link #attempt(InetAddress)} method because
|
||||
* it would take up too many resources.
|
||||
*/
|
||||
public void cleanUpCache() {
|
||||
expiringCache.cleanUp(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player has tried verifying too fast
|
||||
*
|
||||
@@ -39,11 +48,9 @@ public final class FallbackRatelimiter {
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Clean up the cache
|
||||
expiringCache.cleanUp(false);
|
||||
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!expiringCache.has(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
+4
-3
@@ -22,6 +22,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ProtocolVersion {
|
||||
UNKNOWN(-1),
|
||||
@@ -64,9 +65,9 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_3(761),
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764);
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765);
|
||||
|
||||
@Getter
|
||||
private final int protocol;
|
||||
|
||||
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
|
||||
@@ -95,7 +96,7 @@ public enum ProtocolVersion {
|
||||
}
|
||||
|
||||
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
|
||||
LATEST_VERSION = new ArrayList<>(versions).get(versions.size() - 1);
|
||||
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"redundantSuppression"})
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.connection.DownstreamBridge;
|
||||
import net.md_5.bungee.connection.UpstreamBridge;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PacketHandler;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
@@ -60,6 +62,13 @@ public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
|
||||
+11
-15
@@ -20,6 +20,7 @@ package xyz.jonesdev.sonar.bungee.fallback;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
@@ -94,7 +95,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new ConditionFailedException("Duplicate status packet");
|
||||
throw new CorruptedFrameException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
@@ -102,7 +103,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
// The channel always stays open because the client sends
|
||||
// a StatusRequest and a Ping packet after one another
|
||||
if (!isConnected()) {
|
||||
throw new ConditionFailedException("Not connected anymore");
|
||||
throw new CorruptedFrameException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
@@ -119,7 +120,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new ConditionFailedException("Duplicate login packet");
|
||||
throw new CorruptedFrameException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
@@ -223,7 +224,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
@@ -240,12 +241,9 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
pipeline.replace(
|
||||
TIMEOUT_HANDLER,
|
||||
TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
);
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
@@ -304,10 +302,8 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(
|
||||
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, loginRequest.getData(), uuid)
|
||||
));
|
||||
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
@@ -315,7 +311,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
|
||||
+2
-42
@@ -18,14 +18,11 @@
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
@@ -36,8 +33,8 @@ import java.util.Objects;
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
@@ -59,41 +56,4 @@ public final class FallbackListener implements Listener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
|
||||
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
|
||||
// Try to close the channel with a custom serialized disconnect component
|
||||
if (pendingConnection instanceof FallbackInitialHandler) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
|
||||
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
} else {
|
||||
// Fallback by disconnecting without a message
|
||||
pendingConnection.disconnect();
|
||||
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getName())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
|
||||
}
|
||||
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
final Audience audience = Sonar.get().getVerboseHandler().getAudiences().get(event.getPlayer().getName());
|
||||
if (audience != null) {
|
||||
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
|
||||
audience.sendMessage(deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -24,20 +24,21 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
private final @NotNull InetAddress inetAddress;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
|
||||
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Make sure other handlers also know we've disconnected
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
}
|
||||
+155
-33
@@ -20,20 +20,25 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Setter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -44,22 +49,28 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
// General
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser<?, ?> user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
|
||||
|
||||
// Checks
|
||||
private short expectedTransactionId;
|
||||
private long expectedKeepAliveId;
|
||||
private int expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets;
|
||||
private int ignoredMovementTicks;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets, ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
@Setter
|
||||
private @NotNull State state = State.LOGIN_ACK;
|
||||
private boolean listenForMovements;
|
||||
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private static final Random RANDOM = new Random();
|
||||
// Map captcha
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
private final SystemTimer actionBar = new SystemTimer();
|
||||
private @Nullable PreparedMapInfo captcha;
|
||||
private int captchaTriesLeft;
|
||||
|
||||
public enum State {
|
||||
// 1.20.2 configuration state
|
||||
@@ -70,6 +81,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION,
|
||||
// Captcha
|
||||
MAP_CAPTCHA,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
@@ -142,10 +155,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
|
||||
if (!v1_20_2) {
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
// (1.20.2 needs the LOGIN_ACK state) and go on with
|
||||
// the flow of the verification.
|
||||
state = State.CLIENT_SETTINGS;
|
||||
}
|
||||
// Select the JoinGame packet for the respective protocol version
|
||||
// Send the JoinGame packet so we can continue the verification
|
||||
user.write(joinGame);
|
||||
if (v1_20_2) {
|
||||
// Perform the transaction check since the ClientSettings
|
||||
@@ -165,21 +179,37 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PositionLook(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, 0f, expectedTeleportId, false
|
||||
));
|
||||
0f, -90f, expectedTeleportId, false));
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
user.delayedWrite(dynamicSpawnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
// 1.20.3 packets introduced game events
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
user.delayedWrite(START_WRITING_CHUNKS);
|
||||
}
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
|
||||
// Switch to captcha state if needed
|
||||
captchaOrFinish();
|
||||
} else {
|
||||
// Make sure the player knows we are checking them
|
||||
user.delayedWrite(youAreBeingChecked);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
|
||||
@@ -216,13 +246,60 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
// We are expecting a captcha code in chat, drop all other packets
|
||||
if (state == State.MAP_CAPTCHA) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof Chat) {
|
||||
final Chat chat = (Chat) packet;
|
||||
Objects.requireNonNull(captcha);
|
||||
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(1000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
|
||||
user.write(new Chat(MiniMessage.miniMessage().deserialize(
|
||||
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
|
||||
// Make sure to reset the timer
|
||||
actionBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Every about 10 seconds
|
||||
if (keepAlive.elapsed(timeout)) {
|
||||
// Send the message again to remind the player
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
|
||||
// Send both packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
// Check if the time limit has exceeded
|
||||
if (login.elapsed(timeout)) {
|
||||
user.getChannel().close();
|
||||
@@ -321,12 +398,6 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Check if the transaction ID is valid
|
||||
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
|
||||
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, send an Abilities packet to the client to make
|
||||
// sure the player falls even in spectator mode.
|
||||
// Then, teleport the player to the spawn position.
|
||||
@@ -416,7 +487,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Check for too many ignored Y ticks
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks();
|
||||
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
|
||||
return;
|
||||
}
|
||||
@@ -437,7 +508,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// The player is colliding to blocks, finish verification
|
||||
if (ground) {
|
||||
finish();
|
||||
captchaOrFinish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -465,17 +536,46 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
tick++;
|
||||
}
|
||||
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
if (expectedState != state) {
|
||||
user.fail("expected " + expectedState + ", got " + state);
|
||||
private void captchaOrFinish() {
|
||||
if (user.getFallback().shouldDoMapCaptcha()) {
|
||||
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
|
||||
// This should not happen, but we have to return if there is no captcha prepared
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
return;
|
||||
}
|
||||
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
|
||||
state = State.MAP_CAPTCHA;
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
|
||||
}
|
||||
// Initialize the map captcha
|
||||
handleMapCaptcha();
|
||||
} else {
|
||||
// Finish the verification
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
private void handleMapCaptcha() {
|
||||
// Reset max tries
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlot(0, 36, 1, 0,
|
||||
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MapInfoPreparer.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).write(user);
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(CAPTCHA_ABILITIES);
|
||||
// Make sure the player knows what to do
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
@@ -498,4 +598,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails the verification if a certain state is unexpected.
|
||||
*
|
||||
* @param expectedState Expected state
|
||||
*/
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a certain condition is met, fails the verification if not.
|
||||
*
|
||||
* @param condition Condition to fail if it's false
|
||||
* @param message Messages displayed in the stacktrace
|
||||
*/
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -33,14 +33,13 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackPacketListener listener;
|
||||
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
|
||||
final @NotNull FallbackPacketListener listener) {
|
||||
this.user = user;
|
||||
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
|
||||
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
|
||||
this.listener = listener;
|
||||
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
@@ -66,8 +65,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// If the packet hasn't been found, skip it
|
||||
// TODO: Can we fail the verification afterwards?
|
||||
// If the packet isn't found, skip it
|
||||
if (packet == null) {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
return;
|
||||
@@ -93,6 +91,9 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
|
||||
// Let our verification handler process the packet
|
||||
listener.handle(packet);
|
||||
|
||||
// Fire channel read to avoid timeout
|
||||
ctx.fireChannelRead(packet);
|
||||
} finally {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
|
||||
+86
-34
@@ -23,7 +23,7 @@ import lombok.Data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
@@ -48,13 +48,13 @@ public enum FallbackPacketRegistry {
|
||||
CONFIG {
|
||||
{
|
||||
clientbound.register(Disconnect.class, Disconnect::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
map(0x01, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(RegistrySync.class, RegistrySync::new,
|
||||
map(0x05, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(RegistryData.class, RegistryData::new,
|
||||
map(0x05, MINECRAFT_1_20_2, true));
|
||||
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
@@ -166,7 +166,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x40, MINECRAFT_1_19_1, true),
|
||||
map(0x3F, MINECRAFT_1_19_3, true),
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true));
|
||||
map(0x45, MINECRAFT_1_20_2, true),
|
||||
map(0x47, MINECRAFT_1_20_3, true));
|
||||
clientbound.register(Transaction.class, Transaction::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -180,19 +181,73 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
map(0x45, MINECRAFT_1_12, true),
|
||||
map(0x46, MINECRAFT_1_12_1, true),
|
||||
map(0x49, MINECRAFT_1_13, true),
|
||||
map(0x4D, MINECRAFT_1_14, true),
|
||||
map(0x4E, MINECRAFT_1_15, true),
|
||||
map(0x42, MINECRAFT_1_16, true),
|
||||
map(0x4B, MINECRAFT_1_17, true),
|
||||
map(0x4A, MINECRAFT_1_19, true),
|
||||
map(0x4D, MINECRAFT_1_19_1, true),
|
||||
map(0x4C, MINECRAFT_1_19_3, true),
|
||||
map(0x50, MINECRAFT_1_19_4, true),
|
||||
map(0x52, MINECRAFT_1_20_2, true),
|
||||
map(0x54, MINECRAFT_1_20_3, true));
|
||||
clientbound.register(MapData.class, MapData::new,
|
||||
map(0x34, MINECRAFT_1_7_2, true),
|
||||
map(0x24, MINECRAFT_1_9, true),
|
||||
map(0x26, MINECRAFT_1_13, true),
|
||||
map(0x27, MINECRAFT_1_15, true),
|
||||
map(0x26, MINECRAFT_1_16, true),
|
||||
map(0x25, MINECRAFT_1_16_2, true),
|
||||
map(0x27, MINECRAFT_1_17, true),
|
||||
map(0x24, MINECRAFT_1_19, true),
|
||||
map(0x26, MINECRAFT_1_19_1, true),
|
||||
map(0x25, MINECRAFT_1_19_3, true),
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(SetSlot.class, SetSlot::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
map(0x17, MINECRAFT_1_13, true),
|
||||
map(0x16, MINECRAFT_1_14, true),
|
||||
map(0x17, MINECRAFT_1_15, true),
|
||||
map(0x16, MINECRAFT_1_16, true),
|
||||
map(0x15, MINECRAFT_1_16_2, true),
|
||||
map(0x16, MINECRAFT_1_17, true),
|
||||
map(0x13, MINECRAFT_1_19, true),
|
||||
map(0x12, MINECRAFT_1_19_3, true),
|
||||
map(0x14, MINECRAFT_1_19_4, true),
|
||||
map(0x15, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(Chat.class, Chat::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true),
|
||||
map(0x0F, MINECRAFT_1_9, true),
|
||||
map(0x0E, MINECRAFT_1_13, true),
|
||||
map(0x0F, MINECRAFT_1_15, true),
|
||||
map(0x0E, MINECRAFT_1_16, true),
|
||||
map(0x0F, MINECRAFT_1_18_2, true),
|
||||
map(0x5F, MINECRAFT_1_19, true),
|
||||
map(0x62, MINECRAFT_1_19_1, true),
|
||||
map(0x60, MINECRAFT_1_19_3, true),
|
||||
map(0x64, MINECRAFT_1_19_4, true),
|
||||
map(0x67, MINECRAFT_1_20_2, true));
|
||||
map(0x02, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_13, false),
|
||||
map(0x0F, MINECRAFT_1_15, false),
|
||||
map(0x0E, MINECRAFT_1_16, false),
|
||||
map(0x0F, MINECRAFT_1_17, false),
|
||||
map(0x5F, MINECRAFT_1_19, false),
|
||||
map(0x62, MINECRAFT_1_19_1, false),
|
||||
map(0x60, MINECRAFT_1_19_3, false),
|
||||
map(0x64, MINECRAFT_1_19_4, false),
|
||||
map(0x67, MINECRAFT_1_20_2, false),
|
||||
map(0x69, MINECRAFT_1_20_3, false));
|
||||
clientbound.register(GameEvent.class, GameEvent::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true));
|
||||
|
||||
serverbound.register(Chat.class, Chat::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
map(0x03, MINECRAFT_1_12, false),
|
||||
map(0x02, MINECRAFT_1_12_1, false),
|
||||
map(0x03, MINECRAFT_1_14, false),
|
||||
map(0x04, MINECRAFT_1_19, false),
|
||||
map(0x05, MINECRAFT_1_19_1, false),
|
||||
map(0x05, MINECRAFT_1_19_3, false));
|
||||
serverbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@@ -206,7 +261,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x12, MINECRAFT_1_19_1, false),
|
||||
map(0x11, MINECRAFT_1_19_3, false),
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false));
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
@@ -244,7 +300,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x15, MINECRAFT_1_19_1, false),
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false));
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(Position.class, Position::new,
|
||||
@@ -260,7 +317,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_19_1, false),
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false));
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(Player.class, Player::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
@@ -274,7 +332,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x17, MINECRAFT_1_19_1, false),
|
||||
map(0x16, MINECRAFT_1_19_3, false),
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false));
|
||||
map(0x19, MINECRAFT_1_20_2, false),
|
||||
map(0x1A, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(Transaction.class, Transaction::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -287,7 +346,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false));
|
||||
map(0x23, MINECRAFT_1_20_2, false),
|
||||
map(0x24, MINECRAFT_1_20_3, false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,14 +368,13 @@ public enum FallbackPacketRegistry {
|
||||
private final Map<ProtocolVersion, ProtocolRegistry> versions;
|
||||
|
||||
PacketRegistry() {
|
||||
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
for (ProtocolVersion version : ProtocolVersion.values()) {
|
||||
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
for (final ProtocolVersion version : ProtocolVersion.values()) {
|
||||
if (!version.isLegacy() && !version.isUnknown()) {
|
||||
mutableVersions.put(version, new ProtocolRegistry(version));
|
||||
}
|
||||
}
|
||||
|
||||
versions = Collections.unmodifiableMap(mutableVersions);
|
||||
this.versions = Collections.unmodifiableMap(mutableVersions);
|
||||
}
|
||||
|
||||
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
|
||||
@@ -345,7 +404,7 @@ public enum FallbackPacketRegistry {
|
||||
break;
|
||||
}
|
||||
|
||||
final ProtocolRegistry registry = this.versions.get(protocol);
|
||||
final ProtocolRegistry registry = versions.get(protocol);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol version "
|
||||
+ current.protocolVersion);
|
||||
@@ -374,14 +433,7 @@ public enum FallbackPacketRegistry {
|
||||
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
|
||||
final @NotNull PacketMapping next,
|
||||
final @NotNull ProtocolVersion from) {
|
||||
final ProtocolVersion last = (ProtocolVersion) SUPPORTED_VERSIONS.toArray()[SUPPORTED_VERSIONS.size() - 1];
|
||||
final ProtocolVersion to = current == next ? last : next.protocolVersion;
|
||||
|
||||
if (from.compareTo(to) >= 0 && from != last) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Next mapping version (%s) should be lower then current (%s)", to, from));
|
||||
}
|
||||
return to;
|
||||
return current == next ? LATEST_VERSION : next.protocolVersion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-12
@@ -19,31 +19,41 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f);
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f);
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistrySync();
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistryData();
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L);
|
||||
// Game Event (1.20.3+)
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new GameEvent(13, 0);
|
||||
// Chat
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket youAreBeingChecked;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
// JoinGame
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
// Default Spawn Position
|
||||
public FallbackPacket dynamicSpawnPosition;
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
|
||||
@@ -51,6 +61,13 @@ public class FallbackPreparer {
|
||||
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
|
||||
|
||||
// Captcha position
|
||||
public final FallbackPacket CAPTCHA_POSITION = new PositionLook(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
|
||||
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
|
||||
|
||||
// Blocks
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
@@ -58,8 +75,8 @@ public class FallbackPreparer {
|
||||
public double maxFallDistance;
|
||||
|
||||
public void prepare() {
|
||||
joinGame = new JoinGame(5,
|
||||
Sonar.get().getConfig().getVerification().getGamemode().getId(),
|
||||
joinGame = new JoinGame(0,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
@@ -70,7 +87,7 @@ public class FallbackPreparer {
|
||||
"minecraft:overworld");
|
||||
|
||||
maxFallDistance = 0;
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
|
||||
preparedCachedYMotions = new double[maxMovementTick + 8];
|
||||
|
||||
for (int i = 0; i < preparedCachedYMotions.length; i++) {
|
||||
@@ -81,6 +98,7 @@ public class FallbackPreparer {
|
||||
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
dynamicSpawnPosition = new DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
|
||||
|
||||
// Prepare collision platform positions
|
||||
int index = 0;
|
||||
@@ -90,13 +108,25 @@ public class FallbackPreparer {
|
||||
x + (BLOCKS_PER_ROW / 2),
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0
|
||||
);
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare UpdateSectionBlocks packet
|
||||
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
|
||||
|
||||
// "You are being checked" message
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
|
||||
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
|
||||
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
|
||||
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
|
||||
|
||||
// Precompute captcha answers
|
||||
MapInfoPreparer.prepare();
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -28,7 +28,10 @@ import java.util.function.Function;
|
||||
public enum BlockType {
|
||||
STONE(protocolVersion -> 1),
|
||||
BARRIER(protocolVersion -> {
|
||||
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
|
||||
// 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) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
@@ -73,7 +76,8 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
return 10365;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 10366;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+6
-9
@@ -24,18 +24,15 @@ import org.jetbrains.annotations.NotNull;
|
||||
public final class ChangedBlock {
|
||||
private final @NotNull BlockPosition position;
|
||||
private final BlockType type;
|
||||
private final int legacyChunkPosCrammed;
|
||||
private final int modernChunkPosCrammed;
|
||||
private final int legacyBlockState, blockState;
|
||||
|
||||
public ChangedBlock(final @NotNull BlockPosition position, final BlockType type) {
|
||||
this.position = position;
|
||||
this.type = type;
|
||||
this.legacyChunkPosCrammed = position.getX()
|
||||
- (position.getChunkX() << 4) << 12 | position.getZ()
|
||||
- (position.getChunkZ() << 4) << 8 | position.getY();
|
||||
this.modernChunkPosCrammed = position.getX()
|
||||
- (position.getChunkX() << 4) << 8 | position.getZ()
|
||||
- (position.getChunkZ() << 4) << 4 | position.getY()
|
||||
- ((position.getY() >> 4) << 4);
|
||||
final int x = position.getX() - (position.getChunkX() << 4);
|
||||
final int y = position.getY();
|
||||
final int z = position.getZ() - (position.getChunkZ() << 4);
|
||||
this.legacyBlockState = x << 12 | z << 8 | y;
|
||||
this.blockState = x << 8 | z << 4 | y - ((y >> 4) << 4);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum ItemMapType {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+10
-6
@@ -15,12 +15,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
final class ConditionFailedException extends DecoderException {
|
||||
public ConditionFailedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class MapInfo {
|
||||
private final String answer;
|
||||
private final int columns, rows;
|
||||
private final int x, y;
|
||||
private final byte[] buffer;
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static java.awt.Font.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo.DIMENSIONS;
|
||||
|
||||
@UtilityClass
|
||||
public class MapInfoPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
|
||||
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final int[] FONT_STYLES = new int[]{
|
||||
PLAIN,
|
||||
BOLD,
|
||||
ITALIC,
|
||||
ITALIC | BOLD
|
||||
};
|
||||
|
||||
private final int[][] COLOR_PALETTE = new int[][]{
|
||||
new int[]{ // Blue
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51
|
||||
},
|
||||
new int[]{ // Orange
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
},
|
||||
new int[]{ // Green
|
||||
4,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
},
|
||||
new int[]{ // Black
|
||||
27,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
},
|
||||
new int[]{ // Red
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19
|
||||
}
|
||||
};
|
||||
|
||||
private PreparedMapInfo[] cached;
|
||||
@Getter
|
||||
private int preparedCAPTCHAs;
|
||||
private boolean currentlyPreparing;
|
||||
|
||||
public PreparedMapInfo getRandomCaptcha() {
|
||||
return cached[RANDOM.nextInt(preparedCAPTCHAs)];
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Precomputing map captcha answers...");
|
||||
|
||||
// Make sure we're not running into concurrency issues
|
||||
if (currentlyPreparing) {
|
||||
Sonar.get().getLogger().warn("Did not precompute map captcha answers as another task is running.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset all statistics
|
||||
preparedCAPTCHAs = 0;
|
||||
currentlyPreparing = true;
|
||||
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
|
||||
cached = null;
|
||||
cached = new PreparedMapInfo[config.getPrecomputeAmount()];
|
||||
|
||||
// Prepare fonts from config
|
||||
final String dictionary = config.getDictionary();
|
||||
final List<String> fonts = config.getFonts();
|
||||
if (fonts.isEmpty()) {
|
||||
Sonar.get().getLogger().warn("No fonts found, using fallback font...");
|
||||
fonts.add(DIALOG);
|
||||
}
|
||||
final String[] fontTypes = fonts.toArray(new String[0]);
|
||||
|
||||
// Prepare distortions values from config
|
||||
final double distortionsFactorX = config.getDistortionsFactorX();
|
||||
final double distortionsFactorY = config.getDistortionsFactorY();
|
||||
final double halfDistortionsFactorX = distortionsFactorX / 2D;
|
||||
final double halfDistortionsFactorY = distortionsFactorY / 2D;
|
||||
|
||||
for (int _i = 0; _i < cached.length; _i++) {
|
||||
final int currentIndex = _i;
|
||||
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
// Create image
|
||||
final BufferedImage image = new BufferedImage(DIMENSIONS, DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR);
|
||||
final Graphics2D graphics = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
// Create random font
|
||||
final String fontType = fontTypes[RANDOM.nextInt(fontTypes.length)];
|
||||
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
|
||||
final int fontSize = 27
|
||||
+ (config.isRandomizeFontSize()
|
||||
? RANDOM.nextInt(6) - 2 : 5);
|
||||
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
|
||||
graphics.setFont(answerFont);
|
||||
|
||||
// Build answer to the captcha
|
||||
final StringBuilder answerBuilder = new StringBuilder();
|
||||
for (int _j = 0; _j < 5; _j++) {
|
||||
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
|
||||
}
|
||||
final String answer = answerBuilder.toString();
|
||||
|
||||
// Calculate text position
|
||||
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
|
||||
final int halfWidth = image.getWidth() / 2;
|
||||
final int halfHeight = image.getHeight() / 2;
|
||||
final int spacing = 5;
|
||||
double _x = halfWidth - stringWidth / 2f - spacing;
|
||||
double _y = halfHeight + fontSize / 3f;
|
||||
|
||||
// Draw each character one by one
|
||||
final FontRenderContext fontRenderContext = graphics.getFontRenderContext();
|
||||
for (final char c : answer.toCharArray()) {
|
||||
// Randomize x and y
|
||||
if (config.isRandomizePositions()) {
|
||||
_y += RANDOM.nextInt(8) - 4;
|
||||
}
|
||||
|
||||
// Apply random distortion
|
||||
final double distortionFactorX = RANDOM.nextDouble() * distortionsFactorX - halfDistortionsFactorX;
|
||||
final double distortionFactorY = RANDOM.nextDouble() * distortionsFactorY - halfDistortionsFactorY;
|
||||
|
||||
// Create a GlyphVector for the character
|
||||
final String character = String.valueOf(c);
|
||||
final GlyphVector glyphVector = graphics.getFont().createGlyphVector(fontRenderContext, character);
|
||||
|
||||
// Apply the distortion to the GlyphVector using AffineTransform
|
||||
final AffineTransform transform = AffineTransform.getTranslateInstance(_x, _y);
|
||||
transform.shear(distortionFactorX, distortionFactorY);
|
||||
final Shape distortedCharacter = glyphVector.getOutline();
|
||||
final Shape distorted = transform.createTransformedShape(distortedCharacter);
|
||||
|
||||
// Draw the distorted character
|
||||
graphics.fill(distorted);
|
||||
|
||||
// Update x by width
|
||||
_x += glyphVector.getVisualBounds().getWidth() + spacing;
|
||||
}
|
||||
|
||||
// TODO: Color converter
|
||||
graphics.setColor(Color.RED);
|
||||
|
||||
// Draw random lines
|
||||
for (int i = 0; i < config.getRandomLinesAmount(); i++) {
|
||||
final int startX = RANDOM.nextInt(halfWidth);
|
||||
final int startY = RANDOM.nextInt(halfHeight);
|
||||
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
|
||||
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
|
||||
|
||||
graphics.drawLine(startX, startY, endX, endY);
|
||||
}
|
||||
|
||||
// Draw random ovals
|
||||
for (int i = 0; i < config.getRandomOvalsAmount(); i++) {
|
||||
final int startX = RANDOM.nextInt(halfWidth);
|
||||
final int startY = RANDOM.nextInt(halfHeight);
|
||||
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
|
||||
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
|
||||
|
||||
graphics.drawOval(startX, startY, endX, endY);
|
||||
}
|
||||
|
||||
// Select random color palette
|
||||
final int[] colorPalette = COLOR_PALETTE[currentIndex % COLOR_PALETTE.length];
|
||||
// Select the next color palette for geometry
|
||||
final int[] nextColorPalette = COLOR_PALETTE[(currentIndex + 1) % COLOR_PALETTE.length];
|
||||
// Clear background
|
||||
Arrays.fill(buffer, (byte) 57);
|
||||
// Color every pixel individually
|
||||
final int spacingY = halfHeight - fontSize;
|
||||
for (int x = spacing; x < image.getWidth() - spacing; x++) {
|
||||
for (int y = spacingY; y < image.getHeight() - spacingY; y++) {
|
||||
final int pixel = image.getRGB(x, y);
|
||||
final int index = y * image.getWidth() + x;
|
||||
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
|
||||
// Set color of pixel to random color from the palette
|
||||
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 : RANDOM.nextInt(Byte.MAX_VALUE)
|
||||
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
|
||||
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
|
||||
// Write pixel color to buffer
|
||||
buffer[index] = color;
|
||||
}
|
||||
}
|
||||
// Cache buffer to map
|
||||
cached[currentIndex] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
|
||||
} finally {
|
||||
// Dispose graphics
|
||||
graphics.dispose();
|
||||
// Finished?
|
||||
if (currentIndex == cached.length - 1) {
|
||||
Sonar.get().getLogger().info("Successfully precomputed map captcha answers in {}s!", timer);
|
||||
currentlyPreparing = false;
|
||||
}
|
||||
preparedCAPTCHAs++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.map;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@Getter
|
||||
public final class PreparedMapInfo {
|
||||
public static final int DIMENSIONS = (int) Math.pow(2, 7);
|
||||
public static final int SCALE = DIMENSIONS * DIMENSIONS;
|
||||
|
||||
private final MapInfo info;
|
||||
private final MapData[] legacy;
|
||||
private final MapData modern;
|
||||
|
||||
public PreparedMapInfo(final String answer,
|
||||
final int columns, final int rows,
|
||||
final byte @NotNull [] buffer) {
|
||||
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
|
||||
|
||||
// Prepare 1.7 map data using a grid
|
||||
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
final byte buf = buffer[i];
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
|
||||
}
|
||||
this.legacy = new MapData[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
|
||||
}
|
||||
|
||||
// Prepare 1.8+ map data
|
||||
this.modern = new MapData(info);
|
||||
}
|
||||
|
||||
public void write(final @NotNull FallbackUser<?, ?> user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
for (final MapData data : legacy) {
|
||||
user.delayedWrite(data);
|
||||
}
|
||||
} else {
|
||||
user.delayedWrite(modern);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-11
@@ -18,33 +18,26 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
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.dimension.DimensionRegistry;
|
||||
|
||||
import java.io.IOException;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class RegistrySync implements FallbackPacket {
|
||||
public final class RegistryData implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
|
||||
stream.writeByte(10); // CompoundTag ID
|
||||
BinaryTagTypes.COMPOUND.write(DimensionRegistry.CODEC_1_20, stream);
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Cannot write NBT CompoundTag");
|
||||
}
|
||||
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -31,7 +31,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Abilities implements FallbackPacket {
|
||||
private byte encodedFlags;
|
||||
private int encodedFlags;
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
@Override
|
||||
|
||||
+114
-19
@@ -17,48 +17,88 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class Chat implements FallbackPacket {
|
||||
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
|
||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
public static final byte SYSTEM_TYPE = (byte) 1;
|
||||
public static final byte GAME_INFO_TYPE = (byte) 2;
|
||||
|
||||
private Component component;
|
||||
private byte position;
|
||||
private String message;
|
||||
private byte type;
|
||||
private boolean signedPreview;
|
||||
private boolean unsigned = false;
|
||||
private Instant timestamp;
|
||||
private Instant expiry;
|
||||
private long salt;
|
||||
private boolean signed;
|
||||
private byte[] signature;
|
||||
|
||||
// 1.20.3
|
||||
private BinaryTag nbtMessage;
|
||||
|
||||
// Clientbound LegacyChat
|
||||
public Chat(final Component component) {
|
||||
this(component, SYSTEM_TYPE);
|
||||
}
|
||||
|
||||
// Clientbound LegacyChat
|
||||
public Chat(final Component component, final byte type) {
|
||||
this(component, JSONComponentSerializer.json().serialize(component), type);
|
||||
}
|
||||
|
||||
public Chat(final Component component, final String message, final byte type) {
|
||||
this(component, message, type,
|
||||
false, false, null, null,
|
||||
0L, false, null, serialize(new JsonParser().parse(message)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
writeString(byteBuf, serialized);
|
||||
|
||||
// Message type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(position == 2);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, position);
|
||||
// Serialized message
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, nbtMessage);
|
||||
} else {
|
||||
byteBuf.writeByte(this.position);
|
||||
writeString(byteBuf, message);
|
||||
}
|
||||
|
||||
// UUID
|
||||
// Type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(type == GAME_INFO_TYPE);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, type);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(type);
|
||||
}
|
||||
|
||||
// Sender
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, PLACEHOLDER_UUID);
|
||||
@@ -66,7 +106,62 @@ public final class Chat implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
message = readString(byteBuf, 256);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) {
|
||||
final long expiresAt = byteBuf.readLong();
|
||||
final long saltLong = byteBuf.readLong();
|
||||
final byte[] signatureBytes = readByteArray(byteBuf);
|
||||
|
||||
if (saltLong != 0L && signatureBytes.length > 0) {
|
||||
signature = signatureBytes;
|
||||
expiry = Instant.ofEpochMilli(expiresAt);
|
||||
} else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0
|
||||
|| saltLong == 0L) && signatureBytes.length == 0) {
|
||||
unsigned = true;
|
||||
} else {
|
||||
throw new CorruptedFrameException("Invalid signature");
|
||||
}
|
||||
|
||||
signedPreview = byteBuf.readBoolean();
|
||||
if (signedPreview && unsigned) {
|
||||
throw new CorruptedFrameException("Signature missing");
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
|
||||
final int size = readVarInt(byteBuf);
|
||||
if (size < 0 || size > 5) {
|
||||
throw new CorruptedFrameException("Invalid previous messages");
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
readUUID(byteBuf);
|
||||
readByteArray(byteBuf);
|
||||
}
|
||||
|
||||
if (byteBuf.readBoolean()) {
|
||||
readUUID(byteBuf);
|
||||
readByteArray(byteBuf);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timestamp = Instant.ofEpochMilli(byteBuf.readLong());
|
||||
salt = byteBuf.readLong();
|
||||
signed = byteBuf.readBoolean();
|
||||
if (signed) {
|
||||
byte[] sign = new byte[256];
|
||||
byteBuf.readBytes(sign);
|
||||
signature = sign;
|
||||
} else {
|
||||
signature = new byte[0];
|
||||
}
|
||||
|
||||
readVarInt(byteBuf);
|
||||
byte[] bytes = new byte[DIV_FLOOR];
|
||||
byteBuf.readBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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_14;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class DefaultSpawnPosition implements FallbackPacket {
|
||||
private int x, y, z;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(y);
|
||||
byteBuf.writeInt(z);
|
||||
} else {
|
||||
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
|
||||
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
|
||||
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
|
||||
|
||||
byteBuf.writeLong(encoded);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
byteBuf.writeFloat(0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+14
-5
@@ -17,11 +17,13 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -29,8 +31,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
|
||||
@Getter
|
||||
@@ -39,10 +41,15 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
|
||||
@AllArgsConstructor
|
||||
public final class Disconnect implements FallbackPacket {
|
||||
private @Nullable String reason;
|
||||
private BinaryTag binaryTag;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
writeString(byteBuf, Objects.requireNonNull(reason));
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, binaryTag);
|
||||
} else {
|
||||
writeString(byteBuf, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,6 +58,8 @@ public final class Disconnect implements FallbackPacket {
|
||||
}
|
||||
|
||||
public static @NotNull Disconnect create(final Component component) {
|
||||
return new Disconnect(JSONComponentSerializer.json().serialize(component));
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
final BinaryTag binaryTag = serialize(new JsonParser().parse(serialized));
|
||||
return new Disconnect(serialized, binaryTag);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class GameEvent implements FallbackPacket {
|
||||
private int type;
|
||||
private float value;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(type);
|
||||
byteBuf.writeFloat(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class MapData implements FallbackPacket {
|
||||
private MapInfo mapInfo;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
|
||||
byteBuf.writeByte(0); // scaling
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
} else {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
} else {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
byteBuf.writeByte(mapInfo.getColumns());
|
||||
byteBuf.writeByte(mapInfo.getRows());
|
||||
byteBuf.writeByte(mapInfo.getX());
|
||||
byteBuf.writeByte(mapInfo.getY());
|
||||
|
||||
writeVarInt(byteBuf, mapInfo.getBuffer().length);
|
||||
byteBuf.writeBytes(mapInfo.getBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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 static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetSlot implements FallbackPacket {
|
||||
private int windowId;
|
||||
private int slot;
|
||||
private int count;
|
||||
private int data;
|
||||
private int itemId;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(windowId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
byteBuf.writeShort(slot);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
}
|
||||
byteBuf.writeByte(count);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
byteBuf.writeShort(data);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(-1);
|
||||
} else {
|
||||
byteBuf.writeByte(0);
|
||||
}
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -53,12 +53,12 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
}
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
|
||||
final int id = block.getType().getId(protocolVersion);
|
||||
byteBuf.writeShort(block.getLegacyBlockState());
|
||||
final int blockId = block.getType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
writeVarInt(byteBuf, id);
|
||||
writeVarInt(byteBuf, blockId);
|
||||
} else {
|
||||
final int shiftedBlockId = id << 4;
|
||||
final int shiftedBlockId = blockId << 4;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(shiftedBlockId);
|
||||
} else {
|
||||
@@ -67,8 +67,10 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We only need one Y position
|
||||
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
|
||||
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
|
||||
|
||||
// 1.20+ don't have light update suppression
|
||||
@@ -79,8 +81,9 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
|
||||
final long positionIdValue = shiftedBlockId | block.getBlockState();
|
||||
writeVarLong(byteBuf, positionIdValue);
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ public final class SubcommandRegistryHolder implements SubcommandRegistry {
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new LockdownCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand()
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.subcommand.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "lockdown",
|
||||
description = "Prevent all players from joining"
|
||||
)
|
||||
public final class LockdownCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
|
||||
SONAR.getConfig().getLockdown().setEnabled(newState);
|
||||
SONAR.getConfig().getGeneralConfig().set("lockdown.enabled", newState);
|
||||
SONAR.getConfig().getGeneralConfig().save();
|
||||
|
||||
invocation.getSender().sendMessage(newState
|
||||
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.update;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.utility.component;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.internal.LazilyParsedNumber;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
|
||||
@UtilityClass
|
||||
public class ComponentSerializer {
|
||||
public static BinaryTag serialize(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
final JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
|
||||
|
||||
if (jsonPrimitive.isNumber()) {
|
||||
final Number number = json.getAsNumber();
|
||||
|
||||
if (number instanceof Byte) {
|
||||
return ByteBinaryTag.byteBinaryTag((Byte) number);
|
||||
} else if (number instanceof Short) {
|
||||
return ShortBinaryTag.shortBinaryTag((Short) number);
|
||||
} else if (number instanceof Integer) {
|
||||
return IntBinaryTag.intBinaryTag((Integer) number);
|
||||
} else if (number instanceof Long) {
|
||||
return LongBinaryTag.longBinaryTag((Long) number);
|
||||
} else if (number instanceof Float) {
|
||||
return FloatBinaryTag.floatBinaryTag((Float) number);
|
||||
} else if (number instanceof Double) {
|
||||
return DoubleBinaryTag.doubleBinaryTag((Double) number);
|
||||
} else if (number instanceof LazilyParsedNumber) {
|
||||
return IntBinaryTag.intBinaryTag(number.intValue());
|
||||
}
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
|
||||
} else if (jsonPrimitive.isBoolean()) {
|
||||
return ByteBinaryTag.byteBinaryTag((byte) (jsonPrimitive.getAsBoolean() ? 1 : 0));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
|
||||
}
|
||||
} else if (json instanceof JsonObject) {
|
||||
final CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder();
|
||||
|
||||
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
|
||||
compound.put(property.getKey(), serialize(property.getValue()));
|
||||
}
|
||||
|
||||
return compound.build();
|
||||
} else if (json instanceof JsonArray) {
|
||||
final JsonArray jsonArray = (JsonArray) json;
|
||||
|
||||
if (jsonArray.size() == 0) {
|
||||
return ListBinaryTag.empty();
|
||||
}
|
||||
|
||||
final List<BinaryTag> tagItems = new ArrayList<>(jsonArray.size());
|
||||
BinaryTagType<? extends BinaryTag> listType = null;
|
||||
|
||||
for (final JsonElement jsonEl : jsonArray) {
|
||||
final BinaryTag tag = serialize(jsonEl);
|
||||
tagItems.add(tag);
|
||||
|
||||
if (listType == null) {
|
||||
listType = tag.type();
|
||||
} else if (listType != tag.type()) {
|
||||
listType = BinaryTagTypes.COMPOUND;
|
||||
}
|
||||
}
|
||||
|
||||
switch (listType.id()) {
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return LongArrayBinaryTag.longArrayBinaryTag(longs);
|
||||
case 10://BinaryTagTypes.COMPOUND:
|
||||
tagItems.replaceAll(tag -> {
|
||||
if (tag.type() == BinaryTagTypes.COMPOUND) {
|
||||
return tag;
|
||||
} else {
|
||||
return CompoundBinaryTag.builder().put("", tag).build();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
return ListBinaryTag.listBinaryTag(listType, tagItems);
|
||||
}
|
||||
return EndBinaryTag.endBinaryTag();
|
||||
}
|
||||
}
|
||||
+55
-8
@@ -24,9 +24,7 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.DataOutput;
|
||||
@@ -35,6 +33,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
@@ -49,6 +49,24 @@ public class ProtocolUtil {
|
||||
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
|
||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
||||
|
||||
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
|
||||
return new UUID(byteBuf.readLong(), byteBuf.readLong());
|
||||
}
|
||||
|
||||
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) {
|
||||
return readByteArray(byteBuf, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
|
||||
int length = readVarInt(byteBuf);
|
||||
checkFrame(length >= 0, "Got a negative-length array");
|
||||
checkFrame(length <= cap, "Bad array size");
|
||||
checkFrame(byteBuf.isReadable(length), "Trying to read an array that is too long");
|
||||
byte[] array = new byte[length];
|
||||
byteBuf.readBytes(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
|
||||
final ByteBuf slice = content.slice();
|
||||
try {
|
||||
@@ -119,11 +137,40 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
final DataOutput output = new ByteBufOutputStream(byteBuf);
|
||||
output.writeByte(BinaryTagTypes.COMPOUND.id());
|
||||
BinaryTagTypes.COMPOUND.write(compoundTag, output);
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-4aa8208044741102c6326c7e85086e6fa8fcc7c064f7df6fd0411baf5f2b4504
|
||||
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
|
||||
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
|
||||
// TODO: Find a way to improve this...
|
||||
output.writeByte(binaryTag.type().id());
|
||||
if (binaryTag instanceof CompoundBinaryTag) {
|
||||
CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ByteBinaryTag) {
|
||||
ByteBinaryTag tag = (ByteBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ShortBinaryTag) {
|
||||
ShortBinaryTag tag = (ShortBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof IntBinaryTag) {
|
||||
IntBinaryTag tag = (IntBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof LongBinaryTag) {
|
||||
LongBinaryTag tag = (LongBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof DoubleBinaryTag) {
|
||||
DoubleBinaryTag tag = (DoubleBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof StringBinaryTag) {
|
||||
StringBinaryTag tag = (StringBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof ListBinaryTag) {
|
||||
ListBinaryTag tag = (ListBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
} else if (binaryTag instanceof EndBinaryTag) {
|
||||
EndBinaryTag tag = (EndBinaryTag) binaryTag;
|
||||
tag.type().write(tag, output);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||
}
|
||||
|
||||
@@ -24,25 +24,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@UtilityClass
|
||||
public class VarIntUtil {
|
||||
private static final int[] EXACT_BYTE_LENGTHS = new int[33];
|
||||
|
||||
static {
|
||||
for (int i = 0; i <= 32; ++i) {
|
||||
EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
|
||||
}
|
||||
EXACT_BYTE_LENGTHS[32] = 1;
|
||||
}
|
||||
|
||||
public static int varIntBytes(final int value) {
|
||||
return EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
|
||||
}
|
||||
|
||||
public static void write21BitVarInt(final @NotNull ByteBuf byteBuf, final int value) {
|
||||
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||
byteBuf.writeMedium(w);
|
||||
}
|
||||
|
||||
public static int readVarInt(final ByteBuf byteBuf) {
|
||||
int read = readVarIntSafely(byteBuf);
|
||||
if (read == Integer.MIN_VALUE) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
val velocityVersion = "3.2.0-SNAPSHOT"
|
||||
val velocityVersion = "3.3.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
@@ -26,5 +26,5 @@ tasks {
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
java.targetCompatibility = JavaVersion.VERSION_17
|
||||
+14
-62
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.velocity.fallback;
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
@@ -36,8 +35,6 @@ import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -79,7 +76,6 @@ public final class FallbackListener {
|
||||
private static final MethodHandle INITIAL_CONNECTION;
|
||||
private static final MethodHandle CONNECTED_PLAYER;
|
||||
private static final Field CONNECTION_FIELD;
|
||||
private static Field SESSION_HANDLER_FIELD;
|
||||
|
||||
static {
|
||||
CLOSED_MINECRAFT_CONNECTION = new DummyConnection(null);
|
||||
@@ -97,14 +93,6 @@ public final class FallbackListener {
|
||||
IdentifiedKey.class
|
||||
));
|
||||
|
||||
try {
|
||||
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
|
||||
} catch (Throwable throwable) {
|
||||
// Velocity b266 changed the field name to "activeSessionHandler"
|
||||
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("sessionHandler");
|
||||
}
|
||||
SESSION_HANDLER_FIELD.setAccessible(true);
|
||||
|
||||
CONNECTION_FIELD = InitialLoginSessionHandler.class.getDeclaredField("mcConnection");
|
||||
CONNECTION_FIELD.setAccessible(true);
|
||||
|
||||
@@ -151,8 +139,7 @@ public final class FallbackListener {
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
try {
|
||||
// Backwards compatibility for Velocity b265 and below
|
||||
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
|
||||
val activeSessionHandler = mcConnection.getActiveSessionHandler();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (fallback.getBlacklisted().has(inetAddress)) {
|
||||
@@ -160,7 +147,7 @@ public final class FallbackListener {
|
||||
markConnectionAsDead(activeSessionHandler);
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getBlacklisted(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
inboundConnection.getProtocolVersion(), false
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -182,7 +169,7 @@ public final class FallbackListener {
|
||||
markConnectionAsDead(activeSessionHandler);
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
inboundConnection.getProtocolVersion(), false
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +191,7 @@ public final class FallbackListener {
|
||||
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
inboundConnection.getProtocolVersion(), false
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -215,7 +202,7 @@ public final class FallbackListener {
|
||||
|| fallback.getConnected().containsValue(inetAddress)) {
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
inboundConnection.getProtocolVersion(), false
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -224,14 +211,14 @@ public final class FallbackListener {
|
||||
if (!fallback.getRatelimiter().attempt(inetAddress)) {
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
inboundConnection.getProtocolVersion(), false
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
@@ -243,18 +230,14 @@ public final class FallbackListener {
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(event.getUsername()).matches()) {
|
||||
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
|
||||
mcConnection.getProtocolVersion()));
|
||||
mcConnection.getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
pipeline.replace(
|
||||
READ_TIMEOUT,
|
||||
READ_TIMEOUT,
|
||||
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
);
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(READ_TIMEOUT, READ_TIMEOUT, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// We need to determine if the player is premium before we handle the connection,
|
||||
// so we can create a ConnectedPlayer instance without having to spoof this
|
||||
@@ -284,8 +267,7 @@ public final class FallbackListener {
|
||||
final FallbackUserWrapper user = new FallbackUserWrapper(
|
||||
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
|
||||
mcConnection.getChannel().pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(protocolId)
|
||||
);
|
||||
ProtocolVersion.fromId(protocolId));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
@@ -340,10 +322,8 @@ public final class FallbackListener {
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(
|
||||
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, gameProfile.getName(), connectedPlayer.getUniqueId())
|
||||
));
|
||||
user.getPipeline().replace(MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, gameProfile.getName(), connectedPlayer.getUniqueId())));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
@@ -355,22 +335,6 @@ public final class FallbackListener {
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()
|
||||
&& !event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
connectedPlayer.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getUsername())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = event.getPlayer().getRemoteAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
@@ -385,19 +349,7 @@ public final class FallbackListener {
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
connectedPlayer.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getTooManyOnlinePerIp(), connectedPlayer.getProtocolVersion()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled() && Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
if (event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
|
||||
event.getPlayer().sendMessage(deserialized);
|
||||
Sonar.get().getConfig().getTooManyOnlinePerIp(), connectedPlayer.getProtocolVersion(), false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user