mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a472d3aa | ||
|
|
76f0f4abda | ||
|
|
720e1cb1cb | ||
|
|
a98d9337c7 | ||
|
|
5158111edf | ||
|
|
2ab829b43e | ||
|
|
04bbfde6b6 | ||
|
|
ef8abcec46 | ||
|
|
1fb38ec8d4 | ||
|
|
cc5e958e2a | ||
|
|
1437204a15 | ||
|
|
c96d733920 | ||
|
|
c561b3eeaf | ||
|
|
2310864816 | ||
|
|
55030c5073 | ||
|
|
18bb9557b1 | ||
|
|
5d02050815 | ||
|
|
5914b23ae5 | ||
|
|
7ce56a79ab | ||
|
|
7b17ba7e53 | ||
|
|
8268cda660 | ||
|
|
89d0c7346f | ||
|
|
512faa956e | ||
|
|
1f6781aaa3 | ||
|
|
bf40333de5 | ||
|
|
31b961b577 | ||
|
|
e4821781fa | ||
|
|
bdcb7d28a7 | ||
|
|
433fab10f1 | ||
|
|
014b956f9e | ||
|
|
4be1a970d0 | ||
|
|
bea1d292ee | ||
|
|
468a11e0be | ||
|
|
cf2fce9c29 | ||
|
|
cc52289500 | ||
|
|
ac1f6d616b | ||
|
|
bdd941171e | ||
|
|
1e18a29d1e | ||
|
|
5671c2029e | ||
|
|
32aeee4f06 | ||
|
|
7b5ce41420 | ||
|
|
a66f60f177 | ||
|
|
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 | ||
|
|
0b4394624f | ||
|
|
d8f7e288e8 | ||
|
|
3f3411ca9d | ||
|
|
14ae85384a | ||
|
|
8204477eb6 | ||
|
|
44754e5027 | ||
|
|
847936ed08 |
No files matched your search
+13
-25
@@ -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)
|
||||
@@ -24,7 +25,7 @@
|
||||
|
|
||||
[Discord](https://jonesdev.xyz/discord)
|
||||
|
|
||||
[License](https://github.com/jonesdevelopment/sonar/blob/main/README.md#license)
|
||||
[License](https://github.com/jonesdevelopment/sonar/?tab=readme-ov-file#license)
|
||||
</div>
|
||||
|
||||
## Design and Goal
|
||||
@@ -32,27 +33,21 @@
|
||||
* Effective, lightweight, and easy-to-use
|
||||
* No unnecessary features and clean code
|
||||
* Protection against all kinds of bot attacks
|
||||
* No player should be annoyed by any sort of captcha
|
||||
* No player should be annoyed by any sort of [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)
|
||||
* No sort of checking for VPNs or proxies
|
||||
* Multi-platform support (Velocity, BungeeCord, and Bukkit)
|
||||
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
|
||||
|
||||
## Checks
|
||||
|
||||
- New player logins are queued to prevent spam login attacks.
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
|
||||
all types of bots.
|
||||
- Spigot & BungeeCord
|
||||
- Duplicate login/status packet spam fix
|
||||
- Run server ping handling asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
|
||||
- Sonar queues new player logins to prevent spam login attacks
|
||||
- Sonar checks the handshake packets for legitimacy
|
||||
- Sonar makes sure some packets cannot be duplicated illegitimately
|
||||
|
||||
### Fallback
|
||||
|
||||
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever
|
||||
reaching the backend.
|
||||
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and
|
||||
advanced types of bots.
|
||||
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend.
|
||||
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
* Sends the player to a lightweight fake server when they connect for the first time.
|
||||
* Analyzes if the player is sending the necessary packets.
|
||||
@@ -60,16 +55,11 @@ advanced types of bots.
|
||||
* Checks if the player is obeying client gravity.
|
||||
* Checks if the player is colliding with blocks correctly.
|
||||
|
||||
Fallback also protects from huge spambot attacks since it queues the incoming connections, therefore making it
|
||||
technically impossible to have a ton of bots join the server at the same time.
|
||||
Fallback also protects against huge spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
|
||||
|
||||
### False positives
|
||||
|
||||
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
|
||||
However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. For example, some higher Minecraft versions have a bug where the client sometimes sends a packet out of order. Sonar accounts for that and does not falsely blacklist clients.
|
||||
If you or one of your players experiences a false positive, make sure to report it by opening
|
||||
a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket
|
||||
on [Discord](https://jonesdev.xyz/discord/).
|
||||
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check. However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket on [Discord](https://jonesdev.xyz/discord/).
|
||||
|
||||
## Building
|
||||
|
||||
@@ -79,9 +69,7 @@ You can also take a look at the [gradle documentation](https://docs.gradle.org/c
|
||||
|
||||
## Contributing
|
||||
|
||||
If you are interested in contributing, you can check out
|
||||
the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed
|
||||
instructions.
|
||||
If you are interested in contributing, you can check out the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed instructions.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+3
-7
@@ -13,13 +13,9 @@ If you discover a security vulnerability within Sonar, please follow these steps
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at admin@jonesdev.xyz.
|
||||
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at contact@jonesdev.xyz.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to the following people for their contributions:
|
||||
- [xism4](https://github.com/xism4)
|
||||
- fixed serious issues with packet decode logic on BungeeCord [#5](https://github.com/jonesdevelopment/sonar-legacy/pull/5) (Severity: medium)
|
||||
- fixed a false blacklisting issue when a player looses connection [#9](https://github.com/jonesdevelopment/sonar-legacy/pull/9) (Severity: low)
|
||||
- [NoGodHenry](https://github.com/NoGodHenry)
|
||||
- fixed a ConcurrentModificationException in the configuration [#3](https://github.com/jonesdevelopment/sonar-legacy/pull/3) (Severity: low)
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors),
|
||||
and also those who reported security issues or bugs.
|
||||
@@ -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
|
||||
|
||||
+30
-9
@@ -21,8 +21,8 @@ versioner {
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
|
||||
}
|
||||
|
||||
apply(plugin = "java")
|
||||
@@ -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")
|
||||
@@ -43,10 +43,12 @@ allprojects {
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:$adventureVersion")
|
||||
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
|
||||
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
|
||||
// Library/dependency loading
|
||||
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +71,10 @@ tasks {
|
||||
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("Sonar.jar")
|
||||
archiveFileName.set("${rootProject.name}.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
|
||||
@@ -77,10 +82,26 @@ tasks {
|
||||
|
||||
// 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.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
// 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.Audience")
|
||||
exclude("net.kyori.adventure.title.*")
|
||||
}
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// Relocate dynamically loaded libraries
|
||||
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
// We want to load our own Gson dynamically
|
||||
exclude("com/google/gson/**")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/versions/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.8
|
||||
version=2.0.18
|
||||
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.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "sonar"
|
||||
rootProject.name = "Sonar"
|
||||
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
val path = "sonar-$it"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
repositories {
|
||||
maven(url = "https://jitpack.io/") // simple-yaml
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
@@ -27,10 +29,14 @@ import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEventManager;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
@@ -40,6 +46,12 @@ public interface Sonar {
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* Since we want to use Adventure on every server platform,
|
||||
* we have to use their platform module to support BungeeCord and Bukkit
|
||||
*/
|
||||
Map<String, Audience> AUDIENCES = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@@ -60,12 +72,24 @@ public interface Sonar {
|
||||
|
||||
@NotNull Verbose getVerboseHandler();
|
||||
|
||||
@NotNull Notification getNotificationHandler();
|
||||
|
||||
@NotNull SystemTimer getLaunchTimer();
|
||||
|
||||
@NotNull LibraryManager getLibraryManager();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setVerboseHandler(final @NotNull Verbose verboseHandler);
|
||||
|
||||
/**
|
||||
* Set a custom notification handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setNotificationHandler(final @NotNull Notification notificationHandler);
|
||||
|
||||
/**
|
||||
* Reloads the entire plugin
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,7 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
@@ -42,13 +43,16 @@ public final class AttackTracker implements JVMProfiler {
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private long peakJoinsPerSecond;
|
||||
private long peakConnectionsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
private int successfulVerifications, failedVerifications;
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().estimatedSize();
|
||||
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
@@ -62,7 +66,10 @@ public final class AttackTracker implements JVMProfiler {
|
||||
// An attack has been detected
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().sendAttackNotification();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -73,6 +80,11 @@ public final class AttackTracker implements JVMProfiler {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
|
||||
// Update connections per second peak if necessary
|
||||
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
|
||||
}
|
||||
final double processCPUUsage = getProcessCPUUsage();
|
||||
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
|
||||
// Update cpu usage peak if necessary
|
||||
@@ -95,31 +107,37 @@ public final class AttackTracker implements JVMProfiler {
|
||||
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
|
||||
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
|
||||
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
|
||||
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
|
||||
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
|
||||
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
|
||||
final long verified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long failed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
|
||||
.ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%peak-cps%", peakCPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset the attack status
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,15 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -36,7 +38,9 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
@@ -47,10 +51,11 @@ public interface SonarCommand {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
|
||||
|
||||
COMMAND_DELAY.cleanUp(); // Clean up the cache
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
@@ -59,7 +64,6 @@ public interface SonarCommand {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
@@ -67,7 +71,7 @@ public interface SonarCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(source);
|
||||
COMMAND_DELAY.put(source.getAudience(), timestamp);
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,8 +28,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@@ -51,18 +49,7 @@ public abstract class Subcommand {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
// We perform a regex check beforehand,
|
||||
// so we don't have to create a new InetAddress,
|
||||
// therefore, potentially saving some performance.
|
||||
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
@@ -72,7 +59,7 @@ public abstract class Subcommand {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
enum Language {
|
||||
SYSTEM("system"),
|
||||
AB("Abkhazian"),
|
||||
AA("Afar"),
|
||||
AF("Afrikaans"),
|
||||
SQ("Albanian"),
|
||||
AM("Amharic"),
|
||||
AR("Arabic"),
|
||||
AN("Aragonese"),
|
||||
HY("Armenian"),
|
||||
AS("Assamese"),
|
||||
AE("Avestan"),
|
||||
AY("Aymara"),
|
||||
AZ("Azerbaijani"),
|
||||
BA("Bashkir"),
|
||||
EU("Basque"),
|
||||
BE("Belarusian"),
|
||||
BN("Bengali"),
|
||||
BH("Bihari"),
|
||||
BI("Bislama"),
|
||||
BS("Bosnian"),
|
||||
BR("Breton"),
|
||||
BG("Bulgarian"),
|
||||
MY("Burmese"),
|
||||
CA("Catalan"),
|
||||
CH("Chamorro"),
|
||||
CE("Chechen"),
|
||||
ZH("Chinese"),
|
||||
CU("Slavic"),
|
||||
CV("Chuvash"),
|
||||
KW("Cornish"),
|
||||
CO("Corsican"),
|
||||
HR("Croatian"),
|
||||
CS("Czech"),
|
||||
DA("Danish"),
|
||||
DV("Maldivian"),
|
||||
NL("Dutch"),
|
||||
DZ("Dzongkha"),
|
||||
EN("English"),
|
||||
EO("Esperanto"),
|
||||
ET("Estonian"),
|
||||
FO("Faroese"),
|
||||
FJ("Fijian"),
|
||||
FI("Finnish"),
|
||||
FR("French"),
|
||||
GD("Gaelic"),
|
||||
GL("Galician"),
|
||||
KA("Georgian"),
|
||||
DE("German"),
|
||||
EL("Greek"),
|
||||
GN("Guarani"),
|
||||
GU("Gujarati"),
|
||||
HT("Haitian Creole"),
|
||||
HA("Hausa"),
|
||||
HE("Hebrew"),
|
||||
HZ("Herero"),
|
||||
HI("Hindi"),
|
||||
HO("Hiri Motu"),
|
||||
HU("Hungarian"),
|
||||
IS("Icelandic"),
|
||||
IO("Ido"),
|
||||
ID("Indonesian"),
|
||||
IE("Interlingue"),
|
||||
IU("Inuktitut"),
|
||||
IK("Inupiaq"),
|
||||
GA("Irish"),
|
||||
IT("Italian"),
|
||||
JA("Japanese"),
|
||||
JV("Javanese"),
|
||||
KL("Kalaallisut"),
|
||||
KN("Kannada"),
|
||||
KS("Kashmiri"),
|
||||
KK("Kazakh"),
|
||||
KM("Khmer"),
|
||||
KI("Kikuyu"),
|
||||
RW("Kinyarwanda"),
|
||||
KY("Kirghiz"),
|
||||
KV("Komi"),
|
||||
KO("Korean"),
|
||||
KJ("Kwanyama"),
|
||||
KU("Kurdish"),
|
||||
LO("Lao"),
|
||||
LA("Latin"),
|
||||
LV("Latvian"),
|
||||
LI("Limburgish"),
|
||||
LN("Lingala"),
|
||||
LT("Lithuanian"),
|
||||
LB("Luxembourgish"),
|
||||
MK("Macedonian"),
|
||||
MG("Malagasy"),
|
||||
MS("Malay"),
|
||||
ML("Malayalam"),
|
||||
MT("Maltese"),
|
||||
GV("Manx"),
|
||||
MI("Maori"),
|
||||
MR("Marathi"),
|
||||
MH("Marshallese"),
|
||||
MO("Moldavian"),
|
||||
MN("Mongolian"),
|
||||
NA("Nauru"),
|
||||
NV("Navajo"),
|
||||
ND("Ndebele, North"),
|
||||
NR("Ndebele, South"),
|
||||
NG("Ndonga"),
|
||||
NE("Nepali"),
|
||||
SE("Northern Sami"),
|
||||
NO("Norwegian"),
|
||||
NB("Norwegian Bokmal"),
|
||||
NN("Norwegian Nynorsk"),
|
||||
NY("Chewa"),
|
||||
OC("Occitan"),
|
||||
OR("Oriya"),
|
||||
OM("Oromo"),
|
||||
OS("Ossetian"),
|
||||
PI("Pali"),
|
||||
PA("Panjabi"),
|
||||
FA("Persian"),
|
||||
PL("Polish"),
|
||||
PT("Portuguese"),
|
||||
PS("Pushto"),
|
||||
QU("Quechua"),
|
||||
RM("Raeto-Romance"),
|
||||
RO("Romanian"),
|
||||
RN("Rundi"),
|
||||
RU("Russian"),
|
||||
SM("Samoan"),
|
||||
SG("Sango"),
|
||||
SA("Sanskrit"),
|
||||
SC("Sardinian"),
|
||||
SR("Serbian"),
|
||||
SN("Shona"),
|
||||
II("Sichuan Yi"),
|
||||
SD("Sindhi"),
|
||||
SI("Sinhala"),
|
||||
SK("Slovak"),
|
||||
SL("Slovenian"),
|
||||
SO("Somali"),
|
||||
ST("Sotho, Southern"),
|
||||
ES("Spanish"),
|
||||
SU("Sundanese"),
|
||||
SW("Swahili"),
|
||||
SS("Swati"),
|
||||
SV("Swedish"),
|
||||
TL("Tagalog"),
|
||||
TY("Tahitian"),
|
||||
TG("Tajik"),
|
||||
TA("Tamil"),
|
||||
TT("Tatar"),
|
||||
TE("Telugu"),
|
||||
TH("Thai"),
|
||||
BO("Tibetan"),
|
||||
TI("Tigrinya"),
|
||||
TO("Tonga"),
|
||||
TS("Tsonga"),
|
||||
TN("Tswana"),
|
||||
TR("Turkish"),
|
||||
TK("Turkmen"),
|
||||
TW("Twi"),
|
||||
UG("Uighur"),
|
||||
UK("Ukrainian"),
|
||||
UR("Urdu"),
|
||||
UZ("Uzbek"),
|
||||
VI("Vietnamese"),
|
||||
VO("Volapuk"),
|
||||
WA("Walloon"),
|
||||
CY("Welsh"),
|
||||
FY("Western Frisian"),
|
||||
WO("Wolof"),
|
||||
XH("Xhosa"),
|
||||
YI("Yiddish"),
|
||||
YO("Yoruba"),
|
||||
ZA("Zhuang"),
|
||||
ZU("Zulu");
|
||||
|
||||
private final String name;
|
||||
private final String code;
|
||||
|
||||
Language(final String name) {
|
||||
this.name = name;
|
||||
this.code = name().toLowerCase();
|
||||
}
|
||||
|
||||
public static Language fromCode(final @NotNull String code) {
|
||||
return Language.valueOf(code.toUpperCase());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,64 +19,69 @@ package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
|
||||
import org.simpleyaml.configuration.MemorySection;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
|
||||
|
||||
@Getter
|
||||
public final class SimpleYamlConfig {
|
||||
private final File file;
|
||||
private final YamlFile yaml;
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
String.format("Running Sonar version %s on %s",
|
||||
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
|
||||
"Need help or have questions? https://jonesdev.xyz/discord",
|
||||
"https://github.com/jonesdevelopment/sonar",
|
||||
""
|
||||
);
|
||||
|
||||
public SimpleYamlConfig(final File dataFolder, final String fileName) {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
|
||||
public SimpleYamlConfig(final @NotNull File file) {
|
||||
this.file = file;
|
||||
this.yaml = new YamlFile(file.getPath());
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
yaml.save();
|
||||
} catch (IOException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
private void replaceConfigFile(final @NotNull URL defaultData) throws Exception {
|
||||
try (final InputStream inputStream = defaultData.openStream()) {
|
||||
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() throws Exception {
|
||||
// https://github.com/jonesdevelopment/sonar/issues/26
|
||||
// Only load the configuration if the file already exists
|
||||
if (yaml.exists()) {
|
||||
yaml.loadWithComments();
|
||||
} else {
|
||||
yaml.createOrLoadWithComments();
|
||||
public void load(final @NotNull URL defaultData) throws Exception {
|
||||
final boolean exists = yaml.exists();
|
||||
|
||||
// Copy the configuration from the translations
|
||||
if (!exists) {
|
||||
replaceConfigFile(defaultData);
|
||||
}
|
||||
|
||||
// Always load the comment format and options
|
||||
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
|
||||
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
|
||||
// Load the configuration
|
||||
yaml.loadWithComments();
|
||||
|
||||
// I hate my life... but this works... somehow
|
||||
if (exists) {
|
||||
// First, we store all values from the current (old) config
|
||||
final Map<String, Object> values = yaml.getValues(true);
|
||||
// Then, we replace the old config file with the default one
|
||||
replaceConfigFile(defaultData);
|
||||
// Re-load the configuration
|
||||
yaml.loadWithComments();
|
||||
// Loop through the new values from the new configuration
|
||||
yaml.getValues(true).forEach((path, v) -> {
|
||||
final Object value = values.get(path);
|
||||
// Make sure we actually need to update the value
|
||||
if (value != null && !value.equals(v)
|
||||
&& !(v instanceof MemorySection)
|
||||
&& !(value instanceof MemorySection)) {
|
||||
// Set the old values to the new values
|
||||
yaml.set(path, value);
|
||||
}
|
||||
});
|
||||
// Save the yaml file to ensure we keep the original values
|
||||
yaml.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void set(final String path, final Object v) {
|
||||
@@ -88,28 +93,81 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getInt(path, def);
|
||||
}
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return 0;
|
||||
}
|
||||
return yaml.getInt(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path, final boolean def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getBoolean(path, def);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return false;
|
||||
}
|
||||
return yaml.getBoolean(path);
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
|
||||
LOGGER.info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = getObject(path);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
throw new IllegalStateException("Invalid entry " + path);
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public Object getObject(final String path) {
|
||||
return yaml.get(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+109
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,19 +19,24 @@ package xyz.jonesdev.sonar.api.controller;
|
||||
|
||||
import com.j256.ormlite.dao.Dao;
|
||||
import com.j256.ormlite.dao.DaoManager;
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.stmt.QueryBuilder;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
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;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -41,6 +46,24 @@ public final class VerifiedPlayerController {
|
||||
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
|
||||
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[database] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@@ -53,11 +76,11 @@ public final class VerifiedPlayerController {
|
||||
|
||||
// Don't establish a database connection if the type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
|
||||
LOGGER.warn("Configure a database to save verified players.");
|
||||
return;
|
||||
}
|
||||
|
||||
try (final ConnectionSource connectionSource = DependencyLoader.setUpDriverAndConnect()) {
|
||||
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
|
||||
this.connectionSource = connectionSource;
|
||||
|
||||
// Create table
|
||||
@@ -72,13 +95,89 @@ public final class VerifiedPlayerController {
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
// Make sure to run the clean task and the caching task in the same thread
|
||||
// https://github.com/jonesdevelopment/sonar/issues/150
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
try {
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Could not cache entries: {}", exception);
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
|
||||
LOGGER.error("Error setting up database: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets up the database connection using the configured credentials
|
||||
*/
|
||||
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
// Prepare the JDBC connection string
|
||||
// TODO: automatically build best connection string for database type
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
|
||||
|
||||
// Use reflection to uncover the driver class
|
||||
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
|
||||
|
||||
// Make sure we send the username and password
|
||||
final Properties credentials = new Properties();
|
||||
credentials.put("user", database.getUsername());
|
||||
credentials.put("password", database.getPassword());
|
||||
|
||||
// Return new JDBC connection instance for our controller to handle
|
||||
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses reflection to load the database driver from the injected library
|
||||
*
|
||||
* @param connectionString URL of the database
|
||||
* @param driverClass Class of the database driver
|
||||
* @param credentials Credentials for the database
|
||||
*/
|
||||
private static Connection invoke(final @NotNull String connectionString,
|
||||
final @NotNull Class<?> driverClass,
|
||||
final @NotNull Properties credentials) throws Throwable {
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
return (Connection) connect.invoke(driver, connectionString, credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
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);
|
||||
}
|
||||
LOGGER.info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to clear old entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First, remove the player from the local cache and then,
|
||||
* secondly, asynchronously add the player to the database.
|
||||
@@ -111,7 +210,7 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
|
||||
LOGGER.error("Error trying to remove entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -149,7 +248,7 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
dao.create(player);
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
|
||||
LOGGER.error("Error trying to add entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -193,7 +292,7 @@ public final class VerifiedPlayerController {
|
||||
dao.deleteBuilder().delete();
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
|
||||
LOGGER.error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +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.api.dependencies;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
|
||||
public enum Dependency {
|
||||
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.2.0"
|
||||
),
|
||||
// TODO: MariaDB still seems to have some issues; investigate!
|
||||
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
|
||||
MARIADB(
|
||||
"org/mariadb/jdbc",
|
||||
"mariadb-java-client",
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
final @NotNull String version) {
|
||||
final String archiveFileName = String.format("%s-%s.jar", artifactId, version);
|
||||
this.tempFilePath = new File(Sonar.get().getDataDirectory(), "libraries/" + archiveFileName).toPath();
|
||||
|
||||
try {
|
||||
final String mvnCentralPath = String.format("%s/%s/%s/%s-%s.jar",
|
||||
groupId, artifactId, version, artifactId, version);
|
||||
|
||||
this.mvnRepoURL = new URL("https://repo1.maven.org/maven2/" + mvnCentralPath);
|
||||
} catch (MalformedURLException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL for the local file used for the external URLClassLoader
|
||||
*/
|
||||
public @NotNull URL getClassLoaderURL() throws MalformedURLException {
|
||||
if (!Files.exists(tempFilePath)) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Downloading dependency {}...", tempFilePath.getFileName());
|
||||
|
||||
try (final InputStream inputStream = mvnRepoURL.openStream()) {
|
||||
Files.createDirectories(tempFilePath.getParent());
|
||||
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
|
||||
}
|
||||
|
||||
return tempFilePath.toUri().toURL();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +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.api.dependencies;
|
||||
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Driver;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.Properties;
|
||||
|
||||
@UtilityClass
|
||||
public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
|
||||
final URL url = database.getType().getDependency().getClassLoaderURL();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
final Connection connection = connect(classLoader, jdbcURL, database);
|
||||
return new JdbcSingleConnectionSource(jdbcURL, connection);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration.Database database) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.put("user", database.getUsername());
|
||||
properties.put("password", database.getPassword());
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
return (Connection) connect.invoke(driver, databaseURL, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +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.api.dependencies;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
final class ExternalClassLoader extends URLClassLoader {
|
||||
|
||||
/**
|
||||
* Create a URLClassLoader with the system ClassLoader as parent
|
||||
*
|
||||
* @param url URL for of the local file
|
||||
*/
|
||||
public ExternalClassLoader(final URL[] url) {
|
||||
super(url, ClassLoader.getSystemClassLoader().getParent());
|
||||
}
|
||||
|
||||
// Register this URLClassLoader as parallel capable
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -31,5 +31,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -30,11 +30,11 @@ import java.util.Optional;
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
|
||||
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -30,5 +30,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -33,6 +33,6 @@ import java.util.UUID;
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
private final long timeTakenToVerify;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,13 +17,12 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.val;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
@@ -32,7 +31,6 @@ import java.net.InetAddress;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -40,11 +38,14 @@ public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L
|
||||
);
|
||||
// Map of all connected usernames and their respective IP addresses (used for fast checking)
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@Setter
|
||||
private Cache<InetAddress, Byte> blacklist;
|
||||
@Setter
|
||||
private long blacklistTime;
|
||||
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
|
||||
@@ -68,7 +69,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());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,6 +21,5 @@ public interface FallbackPipelines {
|
||||
String FALLBACK_HANDLER = "sonar-fallback-handler";
|
||||
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
|
||||
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
|
||||
String FALLBACK_IN_TRAFFIC = "sonar-traffic-in";
|
||||
String FALLBACK_OUT_TRAFFIC = "sonar-traffic-out";
|
||||
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -30,26 +30,11 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackQueue {
|
||||
public static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(16, 0.5f);
|
||||
static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
// Runnable task executed for polling the queue
|
||||
private final Runnable pollTask = () -> {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
@@ -69,5 +54,23 @@ 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();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,19 +17,28 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackRatelimiter {
|
||||
public static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
@Setter
|
||||
private ExpiringCache<InetAddress> expiringCache;
|
||||
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
private Cache<InetAddress, Byte> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player has tried verifying too fast
|
||||
@@ -39,12 +48,10 @@ 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)) {
|
||||
expiringCache.put(inetAddress);
|
||||
if (!expiringCache.asMap().containsKey(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress, (byte) 0);
|
||||
return true;
|
||||
}
|
||||
// Deny the connection attempt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,13 +17,13 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
@@ -31,34 +31,17 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface FallbackUser<X, Y> {
|
||||
@NotNull Fallback getFallback();
|
||||
|
||||
@NotNull X getPlayer();
|
||||
|
||||
@NotNull Y getConnection();
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull Channel getChannel();
|
||||
|
||||
@NotNull ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
*
|
||||
* @param reason Legacy disconnect message string
|
||||
* @see #disconnect(Component)
|
||||
*/
|
||||
default void disconnect(final @NotNull String reason) {
|
||||
disconnect(Component.text(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
@@ -67,20 +50,78 @@ public interface FallbackUser<X, Y> {
|
||||
*/
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
*
|
||||
* @param username Username of the player
|
||||
* @param uuid UUID of the player
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
* @param boss Name of the main pipeline
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
*
|
||||
* @param msg Message to send to the player
|
||||
*/
|
||||
void write(final @NotNull Object msg);
|
||||
default void write(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().writeAndFlush(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a buffered message that will be
|
||||
* sent once all messages are flushed.
|
||||
*/
|
||||
void delayedWrite(final @NotNull Object msg);
|
||||
default void delayedWrite(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().write(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
|
||||
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Increments the number of times this user (by IP address) has failed the verification
|
||||
*/
|
||||
default void incrementFails() {
|
||||
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
|
||||
final int failCount = getFailedCount();
|
||||
// Make sure to remove the old values from the cache
|
||||
if (failCount > 0) {
|
||||
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
|
||||
}
|
||||
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of times this user (by IP address) has failed the verification
|
||||
* @apiNote Returns 0 if no key/value is present in the cache
|
||||
*/
|
||||
default int getFailedCount() {
|
||||
// Make sure to clean up the cache before we try to get cached values
|
||||
GLOBAL_FAIL_COUNT.cleanUp();
|
||||
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all previously failed verifications
|
||||
*/
|
||||
default void invalidateFails() {
|
||||
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
@@ -95,10 +136,15 @@ public interface FallbackUser<X, Y> {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
// Only log the failed message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,21 +153,32 @@ public interface FallbackUser<X, Y> {
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
// Use a label, so we can easily add more code beneath this method
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Check if the player has too many failed attempts
|
||||
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
|
||||
final int failCount = getFailedCount() + 1;
|
||||
// Now we simply need to check if the threshold is reached
|
||||
if (failCount >= blacklistThreshold) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress().toString());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
} else {
|
||||
// Cache the InetAddress for 3 minutes
|
||||
PREVIOUS_FAILS.put(getInetAddress().toString());
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
invalidateFails();
|
||||
break blacklist;
|
||||
}
|
||||
|
||||
// Make sure we increment the number of fails
|
||||
incrementFails();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback.traffic;
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
* This counts all incoming and outgoing traffic.
|
||||
*/
|
||||
@Getter
|
||||
public enum TrafficCounter implements JVMProfiler {
|
||||
public enum Bandwidth implements JVMProfiler {
|
||||
INCOMING,
|
||||
OUTGOING;
|
||||
|
||||
@@ -48,7 +48,7 @@ public enum TrafficCounter implements JVMProfiler {
|
||||
* This method is called every second.
|
||||
*/
|
||||
public static synchronized void reset() {
|
||||
for (final TrafficCounter value : values()) {
|
||||
for (final Bandwidth value : values()) {
|
||||
value.cacheAndReset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface Counters {
|
||||
@ApiStatus.Internal
|
||||
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(1))
|
||||
.build();
|
||||
|
||||
@ApiStatus.Internal
|
||||
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(1))
|
||||
.build();
|
||||
|
||||
static long getConnectionsPerSecond() {
|
||||
return CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
}
|
||||
|
||||
static long getLoginsPerSecond() {
|
||||
return LOGINS_PER_SECOND.estimatedSize();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
|
||||
public void sendAttackNotification() {
|
||||
// No need to do anything if we have no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Prepare the title
|
||||
// TODO: Why do custom title times not work?
|
||||
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
|
||||
final Title title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
// Prepare the chat message
|
||||
final Component chat = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationChat()
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
|
||||
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
|
||||
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
|
||||
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl()));
|
||||
// Send the title and chat messages to all online players
|
||||
for (final String subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.AUDIENCES.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.showTitle(title);
|
||||
audience.sendMessage(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface Observable {
|
||||
Collection<String> getSubscribers();
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
default boolean isSubscribed(final @NotNull String name) {
|
||||
return getSubscribers().contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
default void subscribe(final String name) {
|
||||
getSubscribers().add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
default void unsubscribe(final String name) {
|
||||
getSubscribers().remove(name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,36 +23,32 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements JVMProfiler {
|
||||
public final class Verbose implements Observable, JVMProfiler, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
loginsPerSecond.cleanUp(false);
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Clean up all counters
|
||||
LOGINS_PER_SECOND.cleanUp();
|
||||
CONNECTIONS_PER_SECOND.cleanUp();
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
@@ -60,7 +56,7 @@ public final class Verbose implements JVMProfiler {
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = audiences.get(subscriber);
|
||||
final Audience audience = Sonar.AUDIENCES.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
@@ -76,9 +72,10 @@ public final class Verbose implements JVMProfiler {
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.estimatedSize()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
@@ -100,26 +97,4 @@ public final class Verbose implements JVMProfiler {
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -7,12 +7,13 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
name = "Sonar"
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
@@ -31,14 +32,17 @@ dependencies {
|
||||
compileOnly(project(":common"))
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bukkit:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,12 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import com.alessiodp.libby.BukkitLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
@@ -34,7 +37,9 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
super(plugin, new BukkitLibraryManager(
|
||||
plugin, plugin.getDataFolder().getName()),
|
||||
plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -78,5 +83,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Bandwidth::reset, 20L, 20L);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
getFallback().getQueue().getPollTask(), 10L, 10L);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Sonar.get().getVerboseHandler()::update, 4L, 4L);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -29,14 +29,14 @@ 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);
|
||||
Sonar.AUDIENCES.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());
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -5,10 +5,11 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
name = "Sonar"
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -19,11 +20,14 @@ dependencies {
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,25 +17,29 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import com.alessiodp.libby.BungeeLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -78,12 +82,24 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Bandwidth::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -29,14 +29,14 @@ 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);
|
||||
Sonar.AUDIENCES.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());
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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);
|
||||
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.protocol.DefinedPacket;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Channel channel;
|
||||
private @Nullable String username;
|
||||
private ProtocolVersion protocolVersion;
|
||||
private InetAddress inetAddress;
|
||||
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private static final Cache<Component, Kick> CACHED_KICK_PACKETS = Caffeine.newBuilder().build();
|
||||
|
||||
private static final Field CHANNEL_WRAPPER_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
|
||||
CHANNEL_WRAPPER_FIELD.setAccessible(true);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull Kick getCachedKickPacket(final Component component) {
|
||||
Kick packet = CACHED_KICK_PACKETS.asMap().get(component);
|
||||
if (packet == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
final BaseComponent baseComponent = ComponentSerializer.deserialize(serialized);
|
||||
packet = new Kick(baseComponent);
|
||||
CACHED_KICK_PACKETS.put(component, packet);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// The player can disconnect without sending the login packet first
|
||||
if (username != null) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
}
|
||||
// The player cannot be in the queue if the IP address is invalid
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
final DefinedPacket wrappedPacket = packetWrapper.packet;
|
||||
// Don't handle any invalid packets
|
||||
if (wrappedPacket != null) {
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
handleHandshake((Handshake) wrappedPacket);
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (wrappedPacket instanceof LoginRequest) {
|
||||
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
// We can override the default exceptionCaught method since this handler
|
||||
// will run before the MinecraftConnection knows that there has been an error.
|
||||
// Additionally, this will also run after our custom decoder.
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Throwable cause) throws Exception {
|
||||
// Sonar sometimes uses exceptions to quickly close
|
||||
// the channel and interrupt any other ongoing process.
|
||||
// Simply close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getHost().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull LoginRequest loginRequest,
|
||||
final @NotNull Object wrappedMessage) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
// Store the username
|
||||
username = loginRequest.getData();
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(channel, protocolVersion,
|
||||
Sonar.get().getConfig().getVerification().getInvalidUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private final ChannelInitializer<Channel> originalChannelInitializer;
|
||||
|
||||
@Override
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
// Invoke the original method
|
||||
try {
|
||||
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
|
||||
}
|
||||
}
|
||||
-78
@@ -1,78 +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.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
private ChannelWrapper channel;
|
||||
private PacketHandler handler;
|
||||
|
||||
@Override
|
||||
public void setHandler(final @NotNull PacketHandler handler) {
|
||||
this.handler = handler;
|
||||
super.setHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
|
||||
try {
|
||||
handler.exception(throwable);
|
||||
} catch (Exception exception) {
|
||||
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
channel.markClosed();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
-343
@@ -1,343 +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.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.EncryptionUtil;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new ConditionFailedException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// 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");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new ConditionFailedException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
try {
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
return;
|
||||
}
|
||||
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We cannot allow too many players on our Fallback server
|
||||
// There's technically no reason for limiting this, but we'll better stay safe.
|
||||
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
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(loginRequest.getData()));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
final int limit = bungee.config.getPlayerLimit();
|
||||
if (limit > 0 && bungee.getOnlineCount() >= limit) {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| user.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? 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)
|
||||
));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
if (cachedKickPacket == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (channelWrapper.getHandle().isActive()) {
|
||||
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
channelWrapper.getHandle().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
channelWrapper.getHandle().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@@ -27,20 +27,27 @@ import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@UtilityClass
|
||||
public class BaseInjectionHelper {
|
||||
public void inject(final ChannelInitializer<Channel> childHandler) {
|
||||
public class FallbackInjectionHelper {
|
||||
public void inject() {
|
||||
try {
|
||||
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
|
||||
childField.setAccessible(true);
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
|
||||
|
||||
// Get the base object (the object containing the field)
|
||||
final Object base = unsafe.staticFieldBase(childField);
|
||||
// Get the offset of the static field within its class
|
||||
final long offset = unsafe.staticFieldOffset(childField);
|
||||
|
||||
unsafe.putObject(base, offset, childHandler);
|
||||
// Replace the original channel initializer
|
||||
unsafe.putObject(base, offset, fallbackInitializer);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
@@ -1,99 +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.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 org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.bungee.fallback.FallbackChannelHandler.getCachedKickPacket;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackLoginListener implements Listener {
|
||||
|
||||
private static final Field CHANNEL_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_FIELD.setAccessible(true);
|
||||
} catch (NoSuchFieldException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
final PendingConnection connection = event.getConnection();
|
||||
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
|
||||
ProtocolVersion.fromId(connection.getVersion()), getCachedKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +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.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
|
||||
private final Fallback fallback;
|
||||
private final ChannelWrapper player;
|
||||
private final FallbackInitialHandler connection;
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delayedWrite(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.write(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +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.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
|
||||
|
||||
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
|
||||
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
try {
|
||||
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
|
||||
} catch (ChannelException ignored) {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
|
||||
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
|
||||
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +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.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
|
||||
|
||||
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
|
||||
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
|
||||
private static final KickStringWriter LEGACY_KICK;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field kickField = PipelineUtils.class.getDeclaredField("legacyKicker");
|
||||
kickField.setAccessible(true);
|
||||
LEGACY_KICK = (KickStringWriter) kickField.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
|
||||
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
|
||||
channel.close();
|
||||
} else {
|
||||
try {
|
||||
BASE.initChannel(channel);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +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.bungee.fallback.varint;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
// MessageToMessageDecoder always do release after decoding
|
||||
// https://github.com/jonesdevelopment/sonar-legacy/pull/5
|
||||
return;
|
||||
}
|
||||
|
||||
final VarintByteDecoder reader = new VarintByteDecoder();
|
||||
final int end = byteBuf.forEachByte(reader);
|
||||
|
||||
if (end == -1) {
|
||||
// This is probably a good sign that the buffer was too short or empty
|
||||
// since the ByteBuf cannot hold a proper VarInt.
|
||||
if (reader.getResult() == VarintByteDecoder.DecoderResult.RUN_OF_ZEROES) {
|
||||
byteBuf.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reader.getResult()) {
|
||||
case RUN_OF_ZEROES: {
|
||||
// this will return to the point where the next varInt starts
|
||||
byteBuf.readerIndex(end);
|
||||
break;
|
||||
}
|
||||
|
||||
case SUCCESS: {
|
||||
final int readVarInt = reader.getReadVarInt();
|
||||
|
||||
if (readVarInt < 0) {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
} else if (readVarInt == 0) {
|
||||
// Actually, we don't want to throw an Exception if the packet is empty.
|
||||
// The check would also false flag a lot of legit players since packets
|
||||
// in 1.7 could sometimes be empty.
|
||||
byteBuf.readerIndex(end + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
final int bytesRead = reader.getBytesRead();
|
||||
final int minimumRead = bytesRead + readVarInt;
|
||||
|
||||
if (byteBuf.isReadable(minimumRead)) {
|
||||
out.add(byteBuf.retainedSlice(end + 1, readVarInt));
|
||||
|
||||
byteBuf.skipBytes(minimumRead);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +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.bungee.fallback.varint;
|
||||
|
||||
import io.netty.util.ByteProcessor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
|
||||
@Getter
|
||||
@ToString
|
||||
public final class VarintByteDecoder implements ByteProcessor {
|
||||
private DecoderResult result = DecoderResult.TOO_SMALL;
|
||||
private int readVarInt, bytesRead;
|
||||
|
||||
@Override
|
||||
public boolean process(byte b) throws Exception {
|
||||
if (b == 0 && bytesRead == 0) {
|
||||
result = DecoderResult.RUN_OF_ZEROES;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result == DecoderResult.RUN_OF_ZEROES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
readVarInt |= (b & 0x7F) << bytesRead++ * 7;
|
||||
|
||||
if (bytesRead > 3) {
|
||||
result = DecoderResult.TOO_BIG;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((b & 0x80) != 128) {
|
||||
result = DecoderResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum DecoderResult {
|
||||
SUCCESS,
|
||||
TOO_SMALL,
|
||||
TOO_BIG,
|
||||
RUN_OF_ZEROES
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ dependencies {
|
||||
compileOnly(project(":api"))
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.Library;
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@UtilityClass
|
||||
class LibraryLoader {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager) {
|
||||
libraryManager.addMavenCentral();
|
||||
libraryManager.addJitPack();
|
||||
libraryManager.loadLibraries(
|
||||
// Simple-YAML
|
||||
Library.builder()
|
||||
.groupId("com{}github{}Carleslc{}Simple-YAML")
|
||||
.artifactId("Simple-Yaml")
|
||||
.version("1.8.4")
|
||||
.relocate("com{}simpleyaml", "xyz{}jonesdev{}sonar{}libs{}yaml")
|
||||
.build(),
|
||||
// Gson
|
||||
Library.builder()
|
||||
.groupId("com{}google{}code{}gson")
|
||||
.artifactId("gson")
|
||||
.version("2.10.1")
|
||||
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
|
||||
.build(),
|
||||
// Caffeine
|
||||
Library.builder()
|
||||
.groupId("com.github.ben-manes.caffeine")
|
||||
.artifactId("caffeine")
|
||||
.version("3.1.8")
|
||||
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
|
||||
.build(),
|
||||
// ORMLite
|
||||
Library.builder()
|
||||
.groupId("com{}j256{}ormlite")
|
||||
.artifactId("ormlite-jdbc")
|
||||
.version("6.1")
|
||||
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
|
||||
.build(),
|
||||
// MySQL driver
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("8.3.0")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,29 +17,27 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.SonarSupplier;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.ServiceRepository;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,24 +45,32 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private T plugin;
|
||||
@Setter
|
||||
private Verbose verboseHandler;
|
||||
@Setter
|
||||
private Notification notificationHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final LibraryManager libraryManager;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final LibraryManager libraryManager,
|
||||
final File dataDirectory,
|
||||
final SonarPlatform platform) {
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
|
||||
// Set the plugin instance before anything else
|
||||
// Store our library manager for the API
|
||||
this.libraryManager = libraryManager;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Set the plugin instance
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.platform = platform;
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
}
|
||||
@@ -86,9 +92,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer);
|
||||
getLogger().info("Enabling all tasks and features...");
|
||||
|
||||
// Start all service threads
|
||||
ServiceRepository.register();
|
||||
|
||||
try {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
@@ -113,19 +116,37 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getConfig().load();
|
||||
|
||||
// Warn player if they reloaded and changed the database type
|
||||
if (getVerifiedPlayerController() != null
|
||||
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
|
||||
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
|
||||
if (verifiedPlayerController != null
|
||||
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
|
||||
getLogger().warn("Reloading the server after changing the database type"
|
||||
+ " is generally not recommended as it can sometimes cause data loss.");
|
||||
}
|
||||
|
||||
// Prepare cached packets
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter
|
||||
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
|
||||
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
|
||||
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
|
||||
// Update ratelimiter cache
|
||||
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
|
||||
final boolean blacklistExists = getFallback().getBlacklist() != null;
|
||||
// Make sure the blacklist is only set when we need it to prevent data loss
|
||||
if (!blacklistExists // Make sure we create a new cache if it doesn't exist yet
|
||||
|| getFallback().getBlacklistTime() != blacklistTime) {
|
||||
// Create new cache with the configured blacklist time
|
||||
getFallback().setBlacklist(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
|
||||
.build());
|
||||
// Store the new blacklist time, so we don't have to reset the blacklist every reload
|
||||
getFallback().setBlacklistTime(blacklistTime);
|
||||
// Warn the user about changing the expiry of the blacklist values
|
||||
if (blacklistExists) {
|
||||
getLogger().warn("The blacklist has been reset as the duration of entries has changed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Reinitialize database controller
|
||||
verifiedPlayerController = new VerifiedPlayerController();
|
||||
@@ -134,8 +155,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
public void shutdown() {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
|
||||
// Shut down service threads
|
||||
ServiceRepository.shutdown();
|
||||
// ...
|
||||
|
||||
getLogger().info("Successfully shut down. Goodbye!");
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the incoming traffic by the amount of readable bytes
|
||||
INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg,
|
||||
final ChannelPromise promise) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the outgoing traffic by the amount of readable bytes
|
||||
OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.write(msg, promise);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +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.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
|
||||
@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());
|
||||
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress"})
|
||||
public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.channel = channel;
|
||||
this.pipeline = channel.pipeline();
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom {@link Disconnect} packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
closeWith(channel, protocolVersion, Disconnect.create(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel Channel to close
|
||||
* @param protocolVersion Protocol version
|
||||
* @param msg Packet to close the channel with
|
||||
*/
|
||||
public static void closeWith(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull Object msg) {
|
||||
if (channel.isActive()) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
channel.config().setAutoRead(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
*
|
||||
* @param username Username of the player
|
||||
* @param uuid UUID of the player
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
*/
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss) {
|
||||
// This rarely happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (pipeline.get(boss) != null) {
|
||||
pipeline.remove(boss);
|
||||
}
|
||||
|
||||
// The player has joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
Sonar.get().getFallback().getConnected().put(username, inetAddress);
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(new LoginSuccess(username, uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
|
||||
}
|
||||
}
|
||||
+213
-65
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,46 +20,60 @@ 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.Fallback;
|
||||
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;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private final @NotNull FallbackUser<?, ?> user;
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
// General
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
|
||||
|
||||
// Checks
|
||||
private short expectedTransactionId;
|
||||
private long expectedKeepAliveId;
|
||||
private int expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets;
|
||||
private int ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets, ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY, spawnYPosition;
|
||||
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,11 +84,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION,
|
||||
// Captcha
|
||||
MAP_CAPTCHA,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID playerUuid) {
|
||||
this.user = user;
|
||||
@@ -108,7 +124,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
encoder.updateRegistry(registry);
|
||||
} else {
|
||||
// Something went wrong - the decoder should not be null
|
||||
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
// Close the channel to prevent bypasses or other potential exploits
|
||||
user.getChannel().close();
|
||||
}
|
||||
@@ -142,10 +158,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
|
||||
@@ -158,38 +175,65 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Set the state to TELEPORT to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TELEPORT;
|
||||
// Make sure the player is unable to fly (the player is in spectator mode)
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Generate the current teleport ID
|
||||
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
|
||||
expectedTeleportId = RANDOM.nextInt();
|
||||
// Add a little randomization to the spawn y coordinate
|
||||
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PositionLook(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, 0f, expectedTeleportId, false
|
||||
));
|
||||
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
|
||||
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() {
|
||||
// If we don't have gravity and captcha enabled, simply finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
&& !FALLBACK.shouldDoMapCaptcha()) {
|
||||
// Save some work by finishing before sending even more packets
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
// 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,
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
|
||||
final String locale) {
|
||||
// Check the client locale by performing a simple regex check on it
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
|
||||
}
|
||||
|
||||
private static boolean validateClientBrand(final @NotNull FallbackUser<?, ?> user, final ByteBuf content) {
|
||||
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
|
||||
// We have to catch every DecoderException, so we can fail and punish
|
||||
// the player instead of only disconnecting them due to an exception.
|
||||
try {
|
||||
@@ -197,11 +241,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// already performs these checks by default.
|
||||
final String read = ProtocolUtil.readBrandMessage(content);
|
||||
// Check if the decoded client brand string is too long
|
||||
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
|
||||
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
|
||||
return false;
|
||||
}
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
return !read.equals("Vanilla") // The normal brand is always lowercase
|
||||
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
|
||||
} catch (DecoderException exception) {
|
||||
@@ -212,22 +256,70 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePacketDuringCaptcha(final @NotNull FallbackPacket packet) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// 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 10 seconds
|
||||
if (keepAlive.elapsed(10_000L)) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
// We are expecting a captcha code in chat, drop all other packets
|
||||
if (state == State.MAP_CAPTCHA) {
|
||||
handlePacketDuringCaptcha(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// 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();
|
||||
return;
|
||||
}
|
||||
checkFrame(!login.elapsed(timeout), "time limit exceeded");
|
||||
|
||||
if (packet instanceof LoginAcknowledged) {
|
||||
// Check if we are currently expecting a LoginAcknowledged packet
|
||||
@@ -297,7 +389,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
}
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.PLUGIN_MESSAGE) {
|
||||
@@ -321,18 +415,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;
|
||||
}
|
||||
|
||||
// TODO: implement ChunkData packet for 1.20.2
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
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.
|
||||
@@ -389,7 +471,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
|
||||
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
|
||||
// Then, check if the position is equal to the spawn position
|
||||
&& x == SPAWN_X_POSITION && y == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
@@ -413,7 +495,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
listenForMovements = true;
|
||||
}
|
||||
|
||||
lastY = dynamicSpawnYPosition;
|
||||
lastY = spawnYPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,13 +504,16 @@ 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;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
@@ -437,15 +522,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (tick > maxMovementTick) {
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// The player is colliding to blocks, finish verification
|
||||
if (ground) {
|
||||
finish();
|
||||
// If the collision check is disabled, finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions()) {
|
||||
captchaOrFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the collision check
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player is colliding to blocks, finish verification
|
||||
captchaOrFinish();
|
||||
return;
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
@@ -460,7 +557,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
}
|
||||
|
||||
@@ -471,23 +568,52 @@ 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 (FALLBACK.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() {
|
||||
state = State.SUCCESS;
|
||||
|
||||
user.getFallback().getConnected().remove(username);
|
||||
FALLBACK.getConnected().remove(username);
|
||||
|
||||
// Add verified player to the database
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
|
||||
@@ -500,8 +626,30 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
|
||||
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -31,16 +31,15 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
private final FallbackPacketListener listener;
|
||||
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
|
||||
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();
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+87
-35
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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,15 +61,23 @@ 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 BlockType blockType = BlockType.BARRIER;
|
||||
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
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 +88,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 +99,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 +109,25 @@ public class FallbackPreparer {
|
||||
x + (BLOCKS_PER_ROW / 2),
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0
|
||||
);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, blockType);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+15
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
@@ -26,9 +27,12 @@ import java.util.function.Function;
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum BlockType {
|
||||
STONE(protocolVersion -> 1),
|
||||
STONE(protocolVersion -> 1, 1D),
|
||||
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,13 +77,17 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
return 10365;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 10366;
|
||||
}
|
||||
});
|
||||
}, 1D);
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
private final Function<ProtocolVersion, Integer> idFunction;
|
||||
// TODO: Implement per-version block heights?
|
||||
@Getter
|
||||
private final double blockHeight;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
return idFunction.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+7
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -61,7 +61,7 @@ public final class DimensionRegistry {
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.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);
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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;
|
||||
}
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user