Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6c3dc9d2b | ||
|
|
a3e98e5e51 | ||
|
|
769d1f2ddb | ||
|
|
f977ae4fe5 | ||
|
|
9b11c4c1e0 | ||
|
|
44afb98b3c | ||
|
|
1e246472cc | ||
|
|
77ee422c64 | ||
|
|
756d7e1f89 | ||
|
|
2ac4b7aad3 | ||
|
|
5726e2d3d2 | ||
|
|
7aea5754a9 | ||
|
|
c10adc37be | ||
|
|
465d81cbc2 | ||
|
|
0152e45190 | ||
|
|
029f7920d0 | ||
|
|
0f5184297c | ||
|
|
d15b3c86ad | ||
|
|
0a26cebaa5 | ||
|
|
16cf92cd17 | ||
|
|
2bac0458dd | ||
|
|
de9bb44bd0 | ||
|
|
0137fdf7e9 | ||
|
|
df14e42a0b | ||
|
|
c30dd0225b | ||
|
|
626fa422bc | ||
|
|
f08d9239fe | ||
|
|
1edd71df04 | ||
|
|
aff3bd4a64 | ||
|
|
ce1d623d60 | ||
|
|
77716c0fcc | ||
|
|
c34a3d7dc2 | ||
|
|
4fdb7cc70c | ||
|
|
7ddca3e152 | ||
|
|
e01a226475 | ||
|
|
21d983362e | ||
|
|
ab5203f4c5 | ||
|
|
03c209829c | ||
|
|
af57d6de42 | ||
|
|
d1f8ab4f10 | ||
|
|
c12d77fb8d | ||
|
|
a9a23aaf2f | ||
|
|
58d0c57ee8 | ||
|
|
5546d2695d | ||
|
|
64688ba06b | ||
|
|
e85036dd13 | ||
|
|
2b2b481c14 | ||
|
|
e0cc9bdab3 | ||
|
|
8aa05563d5 | ||
|
|
21eb949bae | ||
|
|
3a978b39ee | ||
|
|
efc435df61 | ||
|
|
8baa920279 | ||
|
|
647931c31e | ||
|
|
c8c3685355 | ||
|
|
b759082416 | ||
|
|
31a09d60ac | ||
|
|
45fb866c1c | ||
|
|
9f8d828622 | ||
|
|
6e78c875a2 | ||
|
|
18486b9286 | ||
|
|
644d1387ce | ||
|
|
e80311bf24 | ||
|
|
f42dd4d39f | ||
|
|
7717fb26a0 | ||
|
|
97ff082fb4 | ||
|
|
9bdb884302 | ||
|
|
19d2c141de | ||
|
|
5ea02039f2 | ||
|
|
5a5c91b52d | ||
|
|
128e4aa0ca | ||
|
|
504e5f86ae | ||
|
|
932849ba22 | ||
|
|
6ba8148604 | ||
|
|
3fd23cb853 | ||
|
|
bed374e0f8 | ||
|
|
bb3706a949 | ||
|
|
4fa3a0a313 | ||
|
|
62ce6ded18 | ||
|
|
7d45b2d0db | ||
|
|
b578f813dc | ||
|
|
2e0f1ad8ff | ||
|
|
dd21a7df58 | ||
|
|
56d5b47bfe | ||
|
|
931cba82ed | ||
|
|
a1fffe2e74 | ||
|
|
263060e517 | ||
|
|
cffe9c4dd7 | ||
|
|
d96fcca78e | ||
|
|
0027f5988b | ||
|
|
d4d3b91240 | ||
|
|
ebc8ade9e4 | ||
|
|
ef846b137b | ||
|
|
0107a7e569 | ||
|
|
1055f695c6 | ||
|
|
b8bb1839a2 | ||
|
|
1d852e4eb2 | ||
|
|
4d35634caa | ||
|
|
15af0f3823 | ||
|
|
998a248819 | ||
|
|
3846007613 | ||
|
|
c5446f9507 | ||
|
|
5a13b54350 | ||
|
|
d516da7f9e | ||
|
|
a58b8c8a83 | ||
|
|
3be87006aa | ||
|
|
c70c81eb11 | ||
|
|
d71334c1b4 | ||
|
|
59f141d1be | ||
|
|
9bc3d73cb7 | ||
|
|
d3aaba4650 | ||
|
|
68d99dd59b | ||
|
|
5f77f21ee9 | ||
|
|
c5e1d12424 | ||
|
|
25dbbf0b1f | ||
|
|
cbf04f6f2e | ||
|
|
8966d7d0e8 | ||
|
|
064a3cee2a | ||
|
|
260af8b2be | ||
|
|
a7d52467b8 | ||
|
|
0ed6b0351c | ||
|
|
a152633b41 | ||
|
|
f618d1ce77 | ||
|
|
ea35904d33 | ||
|
|
ececb66bf1 | ||
|
|
6caa37e3e0 | ||
|
|
a057199e0b | ||
|
|
d0bbcdc600 | ||
|
|
0a5ce856f3 | ||
|
|
261acbd9e2 | ||
|
|
595b1378ab | ||
|
|
3012ed0d7a | ||
|
|
4ccecde901 | ||
|
|
87fcfb68f3 | ||
|
|
a17b726798 | ||
|
|
50d3b43f63 | ||
|
|
0ff0194894 | ||
|
|
279fe2e3e9 | ||
|
|
ffd3a7e5c2 | ||
|
|
4474f44b0a | ||
|
|
b159947d6d | ||
|
|
a29dfa15e4 | ||
|
|
cb9fe75db8 | ||
|
|
23815351f0 | ||
|
|
5a68ea8af6 | ||
|
|
9b54b484fb | ||
|
|
4fa56f2f3c | ||
|
|
51751584dc | ||
|
|
3955f9bc8a | ||
|
|
e333162032 | ||
|
|
b3a7f4de09 | ||
|
|
5752f1e208 | ||
|
|
1c6ba7050d | ||
|
|
340d4a225d | ||
|
|
475bf28ecf | ||
|
|
9c29753084 | ||
|
|
f0f5196fbe | ||
|
|
d98ce28a59 | ||
|
|
e0d5c67cdf | ||
|
|
65e059368c | ||
|
|
b7a031da12 | ||
|
|
e15206a382 | ||
|
|
5e4af3053e | ||
|
|
7bb231ae0d | ||
|
|
a57dfc4e98 | ||
|
|
618db4804b | ||
|
|
613f793dbf | ||
|
|
40964d431c | ||
|
|
b9deff9c0b | ||
|
|
e3f27171cf | ||
|
|
d3601e10fe | ||
|
|
258e425ba1 | ||
|
|
915e3f593a | ||
|
|
ef525ce718 | ||
|
|
0fbb71a09f | ||
|
|
6ebe44bad3 | ||
|
|
153d56eeb4 | ||
|
|
c214d43b3e | ||
|
|
f3444de4b8 | ||
|
|
a5bf0947b5 | ||
|
|
3dd5b345cb | ||
|
|
c7581f51d2 | ||
|
|
d556275bf9 | ||
|
|
ac662433db | ||
|
|
03cb75bf91 | ||
|
|
541933d472 | ||
|
|
8336279136 | ||
|
|
ea9e53a350 | ||
|
|
0477a574e2 | ||
|
|
c8cdba4504 | ||
|
|
83dbfcb8a8 | ||
|
|
5b62396f9b |
No files matched your search
@@ -8,7 +8,6 @@ joining the [Discord](https://jonesdev.xyz/discord).
|
||||
2. Follow the coding guidelines for your code.
|
||||
3. Test your code before committing and pushing it.
|
||||
4. Use [semantic](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) commit messages for your commits.
|
||||
5. Create a feature branch or push to the `dev` branch.
|
||||
|
||||
## Coding Guidelines:
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://docs.jonesdev.xyz/
|
||||
about: Need help configuring Sonar or have questions regarding the installation and usage?
|
||||
- name: Discord
|
||||
url: https://jonesdev.xyz/discord/
|
||||
about: If you have any further questions, please feel free to join the Discord server and open a ticket.
|
||||
+15
-33
@@ -1,12 +1,11 @@
|
||||
<div align="center">
|
||||
<!-- Introduction -->
|
||||
<p>
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server. Supporting every client and server version.
|
||||
<br>
|
||||
An effective and extensible solution for protecting your Minecraft server against all kinds of bot attacks
|
||||
</p>
|
||||
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server.
|
||||
<br>
|
||||
Sonar keeps your server safe from common and even sophisticated bot attacks.
|
||||
<br><br>
|
||||
|
||||
<!-- Badges & icons -->
|
||||
[](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
|
||||
[](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
|
||||
@@ -15,7 +14,6 @@
|
||||
[](https://jonesdev.xyz/discord)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
<br>
|
||||
<br>
|
||||
<!-- Quick navigation -->
|
||||
[Releases](https://github.com/jonesdevelopment/sonar/releases)
|
||||
|
|
||||
@@ -29,7 +27,6 @@
|
||||
</div>
|
||||
|
||||
## Design and Goal
|
||||
|
||||
* Effective, lightweight, and easy-to-use
|
||||
* No unnecessary features and clean code
|
||||
* Protection against all kinds of bot attacks
|
||||
@@ -38,44 +35,29 @@
|
||||
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
|
||||
|
||||
## Checks
|
||||
Sonar 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 simple method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
- [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
|
||||
1. Sonar sends the player to a lightweight fake server when they connect for the first time.
|
||||
2. Sonar verifies that players obey the laws of Minecraft's physics, including gravity and proper block collision.
|
||||
3. Sonar verifies that players send legitimate packets when interacting with vehicles (e.g. boats).
|
||||
4. Sonar makes sure that players send legitimate packets according to the [vanilla Minecraft protocol](<https://wiki.vg/Protocol>).
|
||||
|
||||
### 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.
|
||||
|
||||
* Sends the player to a lightweight fake server when they connect for the first time.
|
||||
* Analyzes if the player is sending the necessary packets.
|
||||
* Analyzes if the player is sending legitimate packets.
|
||||
* Checks if the player is obeying client gravity.
|
||||
* Checks if the player is colliding with blocks correctly.
|
||||
|
||||
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.
|
||||
Sonar also protects against 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. 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/).
|
||||
Sonar 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.
|
||||
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/new/choose) or a ticket on the [Discord server](https://jonesdev.xyz/discord/).
|
||||
|
||||
## Building
|
||||
|
||||
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/sonar/building).
|
||||
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/development/building).
|
||||
<br>
|
||||
You can also take a look at the [gradle documentation](https://docs.gradle.org/current/userguide/userguide.html) for a better understanding of Gradle.
|
||||
|
||||
## 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.
|
||||
|
||||
## License
|
||||
|
||||
Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
||||
|
||||
## Credits
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
@@ -30,6 +30,5 @@ gradlew*
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.log
|
||||
*.yml
|
||||
*.svg
|
||||
*.txt
|
||||
+68
-90
@@ -1,115 +1,93 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
|
||||
}
|
||||
}
|
||||
import net.kyori.indra.git.IndraGitExtension
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||
id("io.toolebox.git-versioner") version "1.6.7"
|
||||
}
|
||||
|
||||
apply(plugin = "io.toolebox.git-versioner")
|
||||
|
||||
versioner {
|
||||
pattern {
|
||||
pattern = "$version-%h-%c-%b"
|
||||
}
|
||||
java
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.indra.git) apply true
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
mavenCentral()
|
||||
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")
|
||||
apply(plugin = "com.github.johnrengelman.shadow")
|
||||
apply(plugin = "com.gradleup.shadow")
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.projectlombok:lombok:1.18.30")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
compileOnly(rootProject.libs.lombok)
|
||||
annotationProcessor(rootProject.libs.lombok)
|
||||
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.30")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
testCompileOnly(rootProject.libs.lombok)
|
||||
testAnnotationProcessor(rootProject.libs.lombok)
|
||||
|
||||
val adventureVersion = "4.15.0"
|
||||
|
||||
// adventure
|
||||
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
|
||||
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:$adventureVersion")
|
||||
|
||||
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
|
||||
|
||||
// Library/dependency loading
|
||||
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
|
||||
compileOnly(rootProject.libs.adventure.minimessage)
|
||||
compileOnly(rootProject.libs.adventure.serializer)
|
||||
compileOnly(rootProject.libs.ormlite)
|
||||
compileOnly(rootProject.libs.caffeine)
|
||||
compileOnly(rootProject.libs.netty)
|
||||
compileOnly(rootProject.libs.libby.core)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
// We want the jar to actually contain our modules
|
||||
implementation(project(":$it"))
|
||||
tasks {
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}-${project.name.replaceFirstChar { it.uppercase() }}.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// Relocate libraries
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
|
||||
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("org.h2", "xyz.jonesdev.sonar.libs.h2")
|
||||
relocate("com.jhlabs", "xyz.jonesdev.sonar.libs.jhlabs")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
|
||||
// Minimize shadowed jar file
|
||||
minimize {
|
||||
exclude(project(":api"))
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
val indra = rootProject.extensions.getByType(IndraGitExtension::class.java)
|
||||
val gitBranch = indra.branchName() ?: "unknown"
|
||||
val gitCommit = indra.commit()?.name?.substring(0, 8) ?: "unknown"
|
||||
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Title"] = rootProject.name
|
||||
attributes["Implementation-Version"] = rootProject.version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
// Include the Git branch and Git commit SHA
|
||||
attributes["Git-Branch"] = gitBranch
|
||||
attributes["Git-Commit"] = gitCommit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
jar {
|
||||
manifest {
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Version"] = version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("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 {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
// This is a small wrapper tasks to simplify the building process
|
||||
register("build-sonar") {
|
||||
dependsOn(clean, shadowJar)
|
||||
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
|
||||
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
|
||||
dependsOn(buildTasks)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fix permissions so we are actually able to run our gradle script
|
||||
chmod +x gradle*
|
||||
# You can also use clean shadowJar if something goes wrong here
|
||||
# e.g. "./gradlew clean shadowJar --stacktrace"
|
||||
./gradlew build-sonar --stacktrace
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.18
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
version=2.1.2
|
||||
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
|
||||
@@ -0,0 +1,38 @@
|
||||
[versions]
|
||||
netty = "4.1.112.Final"
|
||||
adventure = "4.17.0"
|
||||
adventure-platform = "4.3.4"
|
||||
bstats = "3.0.3"
|
||||
libby = "2.0.0-SNAPSHOT"
|
||||
pluginyml = "0.6.0"
|
||||
|
||||
[plugins]
|
||||
indra-git = "net.kyori.indra.git:3.1.3"
|
||||
shadow = "com.gradleup.shadow:8.3.0"
|
||||
pluginyml-bungee = { id = "net.minecrell.plugin-yml.bungee", version.ref = "pluginyml" }
|
||||
pluginyml-bukkit = { id = "net.minecrell.plugin-yml.bukkit", version.ref = "pluginyml" }
|
||||
|
||||
[libraries]
|
||||
netty = { module = "io.netty:netty-all", version.ref = "netty" }
|
||||
adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" }
|
||||
adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
|
||||
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
|
||||
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
|
||||
adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
|
||||
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
|
||||
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
|
||||
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }
|
||||
libby-velocity = { module = "com.alessiodp.libby:libby-velocity", version.ref = "libby" }
|
||||
bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" }
|
||||
bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
|
||||
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
|
||||
imagefilters = "com.jhlabs:filters:2.0.235-1"
|
||||
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
|
||||
annotations = "org.jetbrains:annotations:24.1.0"
|
||||
simpleyaml = "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
||||
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
|
||||
lombok = "org.projectlombok:lombok:1.18.34"
|
||||
bungeecord = "net.md_5:bungeecord-proxy:master-SNAPSHOT"
|
||||
velocity = "com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT"
|
||||
# We have to use 1.8.8 for backwards compatibility
|
||||
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,7 +85,9 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -201,11 +205,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
Vendored
+12
-10
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
rootProject.name = "Sonar"
|
||||
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
sequenceOf("api", "captcha", "common", "bukkit", "bungee", "velocity").forEach {
|
||||
val path = "sonar-$it"
|
||||
val project = ":$it"
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
compileOnly(rootProject.libs.simpleyaml) {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
compileOnly(rootProject.libs.annotations)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,11 +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.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
@@ -29,28 +28,26 @@ 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.statistics.SonarStatistics;
|
||||
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;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
|
||||
|
||||
Gson GENERAL_GSON = new GsonBuilder()
|
||||
.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<>();
|
||||
@ApiStatus.Internal
|
||||
@Nullable Audience audience(final @Nullable UUID uniqueId);
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull Audience sender(final @NotNull Object object);
|
||||
|
||||
/**
|
||||
* @return The platform the plugin is being run on
|
||||
@@ -62,8 +59,6 @@ public interface Sonar {
|
||||
*/
|
||||
@NotNull LoggerWrapper getLogger();
|
||||
|
||||
@NotNull File getDataDirectory();
|
||||
|
||||
@NotNull SonarConfiguration getConfig();
|
||||
|
||||
@NotNull SubcommandRegistry getSubcommandRegistry();
|
||||
@@ -76,7 +71,7 @@ public interface Sonar {
|
||||
|
||||
@NotNull SystemTimer getLaunchTimer();
|
||||
|
||||
@NotNull LibraryManager getLibraryManager();
|
||||
@NotNull SonarStatistics getStatistics();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
@@ -97,7 +92,7 @@ public interface Sonar {
|
||||
|
||||
@NotNull
|
||||
default SonarVersion getVersion() {
|
||||
return SonarVersion.GET;
|
||||
return SonarVersion.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,19 +17,30 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SonarPlatform {
|
||||
BUKKIT("Bukkit", 19110),
|
||||
BUNGEE("BungeeCord", 19109),
|
||||
VELOCITY("Velocity", 19107);
|
||||
BUKKIT("Bukkit", 19110,
|
||||
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
|
||||
pipeline -> "packet_handler"),
|
||||
BUNGEE("BungeeCord", 19109,
|
||||
pipeline -> "packet-encoder",
|
||||
pipeline -> "inbound-boss"),
|
||||
VELOCITY("Velocity", 19107,
|
||||
pipeline -> "minecraft-encoder",
|
||||
pipeline -> "handler");
|
||||
|
||||
private final String displayName;
|
||||
/**
|
||||
* bStats service ID for the respective Sonar platform
|
||||
*/
|
||||
private final int metricsId;
|
||||
private final Function<ChannelPipeline, String> encoderFunction;
|
||||
private final Function<ChannelPipeline, String> handlerFunction;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,35 +18,49 @@
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
@Getter
|
||||
public final class SonarVersion {
|
||||
static final SonarVersion GET = new SonarVersion();
|
||||
private final String semanticVersion, full, formatted, commitSHA;
|
||||
private final int build;
|
||||
private final boolean isOnMainBranch;
|
||||
static final SonarVersion INSTANCE = new SonarVersion();
|
||||
|
||||
private final String version, formatted, gitBranch, gitCommit;
|
||||
|
||||
SonarVersion() {
|
||||
final Package manifest = Sonar.class.getPackage();
|
||||
final String versionString = manifest.getImplementationVersion();
|
||||
// No need to null-check since we'll always just throw an exception if
|
||||
// we can't find the version information in this file.
|
||||
final Manifest manifest = getManifest();
|
||||
|
||||
if (versionString == null) {
|
||||
this.full = "<could not retrieve version>";
|
||||
this.semanticVersion = "<unknown version>";
|
||||
this.commitSHA = "<unknown commit>";
|
||||
this.build = -1;
|
||||
this.isOnMainBranch = false;
|
||||
this.formatted = full;
|
||||
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
|
||||
return;
|
||||
this.version = manifest.getMainAttributes().getValue("Implementation-Version");
|
||||
this.gitBranch = manifest.getMainAttributes().getValue("Git-Branch");
|
||||
this.gitCommit = manifest.getMainAttributes().getValue("Git-Commit");
|
||||
this.formatted = version + " (" + gitCommit + ")";
|
||||
}
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/pull/1336/
|
||||
private static @NotNull Manifest getManifest() {
|
||||
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
|
||||
final URL resource = Sonar.class.getResource(classLocation);
|
||||
|
||||
if (resource == null) {
|
||||
throw new IllegalStateException("Could not find version information. Is the manifest missing?");
|
||||
}
|
||||
|
||||
this.full = versionString;
|
||||
this.semanticVersion = versionString.split("-")[0];
|
||||
this.commitSHA = versionString.split("-")[1];
|
||||
this.build = Integer.parseInt(versionString.split("-")[2]);
|
||||
this.isOnMainBranch = versionString.split("-")[3].equals("main");
|
||||
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
|
||||
final String classFilePath = resource.toString().replace("\\", "/");
|
||||
final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length());
|
||||
final String manifestPath = archivePath + "/META-INF/MANIFEST.MF";
|
||||
|
||||
try (final InputStream stream = new URL(manifestPath).openStream()) {
|
||||
return new Manifest(stream);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -23,16 +23,15 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
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;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackTracker implements JVMProfiler {
|
||||
public final class AttackTracker {
|
||||
public static final AttackTracker INSTANCE = new AttackTracker();
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
@@ -52,9 +51,9 @@ public final class AttackTracker implements JVMProfiler {
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
|
||||
final long joinsPerSecond = Sonar.get().getStatistics().getLoginsPerSecond();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
|
||||
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|
||||
@@ -67,9 +66,9 @@ public final class AttackTracker implements JVMProfiler {
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
|
||||
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().sendAttackNotification();
|
||||
Sonar.get().getNotificationHandler().observe();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -80,7 +79,7 @@ public final class AttackTracker implements JVMProfiler {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
|
||||
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
|
||||
// Update connections per second peak if necessary
|
||||
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
|
||||
@@ -102,42 +101,44 @@ public final class AttackTracker implements JVMProfiler {
|
||||
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
|
||||
|
||||
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
|
||||
// Save attack statistics
|
||||
final long deltaInMillis = currentAttack.duration.delay();
|
||||
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().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().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());
|
||||
}));
|
||||
.ifPresent(webhook -> {
|
||||
// Save attack statistics
|
||||
final long deltaInMillis = currentAttack.duration.delay();
|
||||
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().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 = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
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("<attack-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
|
||||
@@ -150,10 +151,4 @@ public final class AttackTracker implements JVMProfiler {
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -20,13 +20,11 @@ package xyz.jonesdev.sonar.api.command;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSource sender;
|
||||
private final Subcommand subcommand;
|
||||
private final InvocationSource source;
|
||||
private final String[] rawArguments;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -23,34 +23,38 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class InvocationSource {
|
||||
private final String name;
|
||||
public final class InvocationSource {
|
||||
private final UUID uuid;
|
||||
private final Audience audience;
|
||||
private final boolean player;
|
||||
private final Predicate<String> permissionFunction;
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
* @return True, if {@link InvocationSource#uuid} is not null
|
||||
* @apiNote This indicates a player as a player will always have a UUID
|
||||
*/
|
||||
public final void sendMessage() {
|
||||
sendMessage(Component.empty());
|
||||
public boolean isPlayer() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*
|
||||
* @apiNote We should probably use cached components...
|
||||
* (See {@link InvocationSource#sendMessage(Component)})
|
||||
*/
|
||||
public final void sendMessage(final String legacy) {
|
||||
public void sendMessage(final String legacy) {
|
||||
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*/
|
||||
public final void sendMessage(final Component component) {
|
||||
public void sendMessage(final Component component) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,19 +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.event.ClickEvent;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.Tag;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -38,93 +36,65 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
default void handle(final @NotNull InvocationSource source, final String[] args) {
|
||||
if (source.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
COMMAND_DELAY.put(source.getAudience(), timestamp);
|
||||
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
|| Arrays.stream(sub.getInfo().aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
subcommand.ifPresentOrElse(command -> {
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
subcommand.get().invoke(source, args);
|
||||
if (!command.getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(command.getPermission())) {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("permission", command.getPermission())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Invoke subcommand
|
||||
command.invoke(source, args);
|
||||
}, () -> {
|
||||
// Print standard help message
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>",
|
||||
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
|
||||
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
|
||||
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
|
||||
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
source.sendMessage(component);
|
||||
}
|
||||
final Component yes = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.tick"));
|
||||
final Component no = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.cross"));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
|
||||
TagResolver.resolver("suggest-subcommand",
|
||||
(queue, context) -> Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()))),
|
||||
Placeholder.unparsed("subcommand", command.getInfo().name()),
|
||||
Placeholder.unparsed("description", command.getDescription()),
|
||||
Placeholder.unparsed("permission", command.getPermission()),
|
||||
Placeholder.unparsed("aliases", command.getAliases()),
|
||||
Placeholder.component("only-players", command.getInfo().onlyPlayers() ? yes : no),
|
||||
Placeholder.component("only-console", command.getInfo().onlyConsole() ? yes : no)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
@@ -133,12 +103,10 @@ public interface SonarCommand {
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
ARG_TAB_SUGGESTIONS.put(alias, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,8 +115,8 @@ public interface SonarCommand {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
final String subcommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,82 +18,85 @@
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Getter
|
||||
public abstract class Subcommand {
|
||||
private final @NotNull SubcommandInfo info;
|
||||
private final String permission, aliases, arguments;
|
||||
|
||||
protected static final Sonar SONAR = Sonar.get();
|
||||
|
||||
public Subcommand() {
|
||||
info = getClass().getAnnotation(SubcommandInfo.class);
|
||||
permission = "sonar." + info.name();
|
||||
aliases = info.aliases().length == 0 ? "No aliases."
|
||||
this.info = Objects.requireNonNull(getClass().getAnnotation(SubcommandInfo.class));
|
||||
this.permission = "sonar." + info.name();
|
||||
this.aliases = info.aliases().length == 0 ? "No aliases."
|
||||
: String.join(", ", info.aliases());
|
||||
|
||||
arguments = info.arguments().length == 0 ? ""
|
||||
: Arrays.stream(info.arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.joining(", "));
|
||||
this.arguments = info.arguments().length == 0 ? ""
|
||||
: String.join(", ", info.arguments());
|
||||
}
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
private static final Pattern IPv4_REGEX = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
private static final Pattern IPv6_REGEX = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))");
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
protected static @Nullable String validateIP(final InvocationSource source, final String raw) {
|
||||
if (!IPv4_REGEX.matcher(raw).matches() && !IPv6_REGEX.matcher(raw).matches()) {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.invalid-ip-address"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
return raw;
|
||||
}
|
||||
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
|
||||
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")"));
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource) {
|
||||
incorrectUsage(invocationSource, info.name() + " (" + arguments + ")");
|
||||
}
|
||||
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource, final String usage) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.incorrect-usage"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("subcommand-usage", usage)));
|
||||
}
|
||||
|
||||
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
|
||||
// Check if the subcommand can only be executed by players
|
||||
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
if (info.onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.player-only"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the subcommand can only be executed though console
|
||||
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
if (info.onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.console-only"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
|
||||
if (info.argumentsRequired() && info.arguments().length > 0 && arguments.length <= 1) {
|
||||
incorrectUsage(invocationSource);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command
|
||||
execute(commandInvocation);
|
||||
// Execute the sub command from the invocation source with the given arguments
|
||||
execute(new CommandInvocation(invocationSource, arguments));
|
||||
}
|
||||
|
||||
public final @NotNull String getDescription() {
|
||||
final String path = String.format("commands.%s.description", info.name());
|
||||
return Objects.requireNonNull(Sonar.get().getConfig().getMessagesConfig().getString(path));
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
|
||||
+2
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -33,11 +31,6 @@ public @interface SubcommandInfo {
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* @return Description of the subcommand
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* @return Aliases of the subcommand
|
||||
*/
|
||||
@@ -46,7 +39,7 @@ public @interface SubcommandInfo {
|
||||
/**
|
||||
* @return Arguments of the subcommand
|
||||
*/
|
||||
Argument[] arguments() default {};
|
||||
String[] arguments() default {};
|
||||
|
||||
/**
|
||||
* @return Whether the subcommands requires arguments
|
||||
|
||||
+9
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,30 +17,24 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
public interface SubcommandRegistry {
|
||||
List<Subcommand> SUBCOMMANDS = new Vector<>();
|
||||
|
||||
@NotNull
|
||||
@Unmodifiable
|
||||
default List<Subcommand> getSubcommands() {
|
||||
return Collections.unmodifiableList(SUBCOMMANDS);
|
||||
}
|
||||
@Getter
|
||||
public final class SubcommandRegistry {
|
||||
private final List<Subcommand> subcommands = new Vector<>();
|
||||
|
||||
/**
|
||||
* Registers a single or multiple subcommand(s)
|
||||
*
|
||||
* @param subcommand Array of Subcommands to register
|
||||
*/
|
||||
default void register(final Subcommand @NotNull ... subcommand) {
|
||||
SUBCOMMANDS.addAll(Arrays.asList(subcommand));
|
||||
public void register(final Subcommand @NotNull ... subcommand) {
|
||||
subcommands.addAll(Arrays.asList(subcommand));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +43,7 @@ public interface SubcommandRegistry {
|
||||
* @param subcommand Array of Subcommands to unregister
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
default void unregister(final Subcommand @NotNull ... subcommand) {
|
||||
SUBCOMMANDS.removeAll(Arrays.asList(subcommand));
|
||||
public void unregister(final Subcommand @NotNull ... subcommand) {
|
||||
subcommands.removeAll(Arrays.asList(subcommand));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -21,7 +21,7 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
enum Language {
|
||||
public enum Language {
|
||||
SYSTEM("system"),
|
||||
AB("Abkhazian"),
|
||||
AA("Afar"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -88,11 +88,6 @@ public final class SimpleYamlConfig {
|
||||
yaml.set(path, v);
|
||||
}
|
||||
|
||||
public int getInt(final String path, final int def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getInt(path, def);
|
||||
}
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -101,11 +96,6 @@ public final class SimpleYamlConfig {
|
||||
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());
|
||||
@@ -114,42 +104,18 @@ public final class SimpleYamlConfig {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
final Object object = yaml.get(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());
|
||||
@@ -158,11 +124,6 @@ public final class SimpleYamlConfig {
|
||||
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());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,14 +17,20 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import com.alessiodp.libby.Library;
|
||||
import com.j256.ormlite.db.DatabaseType;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.io.File;
|
||||
@@ -41,7 +47,10 @@ import java.util.regex.Pattern;
|
||||
public final class SonarConfiguration {
|
||||
@Getter
|
||||
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
|
||||
private final File languageFile;
|
||||
@Getter
|
||||
private final File languageFile, pluginFolder;
|
||||
@Getter
|
||||
private Language language;
|
||||
|
||||
static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@@ -62,6 +71,7 @@ public final class SonarConfiguration {
|
||||
};
|
||||
|
||||
public SonarConfiguration(final @NotNull File pluginFolder) {
|
||||
this.pluginFolder = pluginFolder;
|
||||
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
|
||||
this.generalConfig = new SimpleYamlConfig(new File(pluginFolder, "config.yml"));
|
||||
this.webhookConfig = new SimpleYamlConfig(new File(pluginFolder, "webhook.yml"));
|
||||
@@ -97,36 +107,44 @@ public final class SonarConfiguration {
|
||||
return Language.fromCode(property);
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.error("Could not find requested language: {}", throwable);
|
||||
LOGGER.error("You can view a full list of valid language codes here:");
|
||||
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
|
||||
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
} catch (IOException exception) {
|
||||
LOGGER.error("Error reading language file: {}", exception);
|
||||
}
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
// Make sure the plugin folder actually exists before trying to copy files into it
|
||||
if (!pluginFolder.exists() && !pluginFolder.mkdirs()) {
|
||||
throw new IllegalStateException("Could not create plugin folder (insufficient permissions?)");
|
||||
}
|
||||
|
||||
// Generate the language file and check what it's set to
|
||||
Language preferredLanguage = getPreferredLanguage();
|
||||
if (preferredLanguage == Language.SYSTEM) {
|
||||
language = getPreferredLanguage();
|
||||
if (language == Language.SYSTEM) {
|
||||
try {
|
||||
// Try using the system language to determine the language file
|
||||
final String property = System.getProperty("user.language", "en");
|
||||
preferredLanguage = Language.fromCode(property);
|
||||
language = Language.fromCode(property);
|
||||
// Make sure the user knows that we're using the system language for translations
|
||||
LOGGER.info("Using system language ({}) for translations.", preferredLanguage);
|
||||
LOGGER.info("Using system language ({}) for translations.", language);
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Could not use system language for translations.");
|
||||
LOGGER.warn("Using default language ({}) for translations.", preferredLanguage);
|
||||
LOGGER.warn("Using default language ({}) for translations.", language);
|
||||
}
|
||||
} else {
|
||||
LOGGER.info("Using custom language ({}) for translations.", preferredLanguage);
|
||||
LOGGER.info("Using custom language ({}) for translations.", language);
|
||||
}
|
||||
|
||||
// Load all configurations
|
||||
try {
|
||||
generalConfig.load(getAsset("config", preferredLanguage));
|
||||
messagesConfig.load(getAsset("messages", preferredLanguage));
|
||||
webhookConfig.load(getAsset("webhook", preferredLanguage));
|
||||
generalConfig.load(getAsset("config", language));
|
||||
messagesConfig.load(getAsset("messages", language));
|
||||
webhookConfig.load(getAsset("webhook", language));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Error loading configuration", exception);
|
||||
}
|
||||
@@ -140,7 +158,7 @@ public final class SonarConfiguration {
|
||||
|
||||
// General settings
|
||||
logPlayerAddresses = generalConfig.getBoolean("general.log-player-addresses");
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 1, 100);
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 0, 99);
|
||||
|
||||
// Attack tracker
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("attack-tracker.min-players-for-attack"), 2, 1024);
|
||||
@@ -150,11 +168,6 @@ public final class SonarConfiguration {
|
||||
|
||||
// Database
|
||||
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
|
||||
database.host = generalConfig.getString("database.host");
|
||||
database.port = generalConfig.getInt("database.port");
|
||||
database.name = generalConfig.getString("database.name");
|
||||
database.username = generalConfig.getString("database.username");
|
||||
database.password = generalConfig.getString("database.password");
|
||||
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age"), 1, 365);
|
||||
|
||||
// Queue
|
||||
@@ -162,45 +175,59 @@ public final class SonarConfiguration {
|
||||
|
||||
// Verification
|
||||
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
|
||||
// Warn the user if the verification timing is set to NEVER
|
||||
if (verification.timing == Verification.Timing.NEVER) {
|
||||
LOGGER.warn(" ");
|
||||
LOGGER.warn("You have set the verification timing to 'NEVER'.");
|
||||
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
|
||||
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
|
||||
LOGGER.warn("Please only edit this option if you really know what you are doing.");
|
||||
LOGGER.warn(" ");
|
||||
}
|
||||
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.collision.enabled");
|
||||
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"), 1, 128);
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
|
||||
|
||||
verification.vehicle.enabled = generalConfig.getBoolean("verification.checks.vehicle.enabled");
|
||||
verification.vehicle.minimumPackets = clamp(generalConfig.getInt("verification.checks.vehicle.minimum-packets"), 0, 20);
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position");
|
||||
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-x"), 0, 100) / 100D;
|
||||
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
|
||||
"verification.checks.map-captcha.distortions-factor-y"), 0, 100) / 100D;
|
||||
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines");
|
||||
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals");
|
||||
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size");
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
|
||||
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 10, 5000);
|
||||
verification.map.maxDuration = clamp(generalConfig.getInt("verification.checks.map-captcha.max-duration"), 5000, 360000);
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts");
|
||||
verification.map.alphabet = generalConfig.getString("verification.checks.map-captcha.alphabet");
|
||||
verification.map.backgroundImage = null;
|
||||
|
||||
final String backgroundPath = generalConfig.getString("verification.checks.map-captcha.background");
|
||||
if (!backgroundPath.isEmpty()) {
|
||||
try (final InputStream inputStream = new FileInputStream(backgroundPath)) {
|
||||
verification.map.backgroundImage = inputStream;
|
||||
} catch (IOException exception) {
|
||||
Sonar.get().getLogger().error("Could not find background image", exception);
|
||||
}
|
||||
}
|
||||
|
||||
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
|
||||
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
|
||||
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
|
||||
|
||||
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping"), 500, 30000);
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
verification.logDuringAttack = generalConfig.getBoolean("verification.log-during-attack");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 500, 30000);
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 1000, 30000);
|
||||
verification.writeTimeout = clamp(generalConfig.getInt("verification.write-timeout"), 1000, 30000);
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay"), 0, 100000);
|
||||
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
|
||||
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
|
||||
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
|
||||
verification.whitelistedProtocols.clear();
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols"));
|
||||
verification.blacklistedProtocols.clear();
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols"));
|
||||
|
||||
@@ -217,7 +244,7 @@ public final class SonarConfiguration {
|
||||
|
||||
webhook.embed.title = webhookConfig.getString(embedPath + "title");
|
||||
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
|
||||
webhook.embed.description = fromList(webhookConfig.getStringList(embedPath + "description"));
|
||||
webhook.embed.description = String.join("\n", webhookConfig.getStringList(embedPath + "description"));
|
||||
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
|
||||
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
|
||||
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
|
||||
@@ -233,88 +260,43 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
// Messages
|
||||
prefix = formatString(messagesConfig.getString("prefix"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
|
||||
header = fromList(messagesConfig.getStringList("header"));
|
||||
footer = fromList(messagesConfig.getStringList("footer"));
|
||||
tooManyOnlinePerIp = deserialize(fromList(messagesConfig.getStringList("too-many-online-per-ip")));
|
||||
|
||||
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
|
||||
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
|
||||
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
|
||||
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address"));
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
|
||||
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
|
||||
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
|
||||
commands.commandCoolDownLeft = formatString(messagesConfig.getString("commands.cool-down-left"));
|
||||
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission"));
|
||||
commands.helpHeader = messagesConfig.getStringList("commands.main.header");
|
||||
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands"));
|
||||
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
|
||||
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
|
||||
commands.reloading = formatString(messagesConfig.getString("commands.reload.start"));
|
||||
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
header = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("header")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
footer = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("footer")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
commands.verboseSubscribed = formatString(messagesConfig.getString("commands.verbose.subscribed"));
|
||||
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed"));
|
||||
tooManyOnlinePerIp = deserializeDisconnectMessage("too-many-online-per-ip");
|
||||
verification.currentlyPreparing = deserializeDisconnectMessage("verification.currently-preparing");
|
||||
verification.tooFastReconnect = deserializeDisconnectMessage("verification.too-fast-reconnect");
|
||||
verification.alreadyVerifying = deserializeDisconnectMessage("verification.already-verifying");
|
||||
verification.alreadyQueued = deserializeDisconnectMessage("verification.already-queued");
|
||||
verification.blacklisted = deserializeDisconnectMessage("verification.blacklisted");
|
||||
verification.invalidUsername = deserializeDisconnectMessage("verification.invalid-username");
|
||||
verification.protocolBlacklisted = deserializeDisconnectMessage("verification.blacklisted-protocol");
|
||||
verification.verificationSuccess = deserializeDisconnectMessage("verification.success");
|
||||
verification.verificationFailed = deserializeDisconnectMessage("verification.failed");
|
||||
|
||||
commands.notificationsSubscribed = formatString(messagesConfig.getString("commands.notify.subscribed"));
|
||||
commands.notificationsUnsubscribed = formatString(messagesConfig.getString("commands.notify.unsubscribed"));
|
||||
verboseAnimation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
}
|
||||
|
||||
commands.blacklistEmpty = formatString(messagesConfig.getString("commands.blacklist.empty"));
|
||||
commands.blacklistCleared = formatString(messagesConfig.getString("commands.blacklist.cleared"));
|
||||
commands.blacklistSize = formatString(messagesConfig.getString("commands.blacklist.size"));
|
||||
commands.blacklistAdd = formatString(messagesConfig.getString("commands.blacklist.added"));
|
||||
commands.blacklistAddWarning = formatString(messagesConfig.getString("commands.blacklist.added-warning"));
|
||||
commands.blacklistRemove = formatString(messagesConfig.getString("commands.blacklist.removed"));
|
||||
commands.blacklistDuplicate = formatString(messagesConfig.getString("commands.blacklist.duplicate-ip"));
|
||||
commands.blacklistNotFound = formatString(messagesConfig.getString("commands.blacklist.ip-not-found"));
|
||||
|
||||
commands.verifiedEmpty = formatString(messagesConfig.getString("commands.verified.empty"));
|
||||
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
|
||||
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
|
||||
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
|
||||
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
|
||||
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
|
||||
|
||||
commands.statisticsHeader = formatString(messagesConfig.getString("commands.statistics.header"));
|
||||
commands.unknownStatisticType = formatString(messagesConfig.getString("commands.statistics.unknown-type"));
|
||||
commands.generalStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.general")));
|
||||
commands.cpuStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.cpu")));
|
||||
commands.memoryStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.memory")));
|
||||
commands.networkStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.network")));
|
||||
|
||||
verification.connectLog = formatString(messagesConfig.getString("verification.logs.connection"));
|
||||
verification.failedLog = formatString(messagesConfig.getString("verification.logs.failed"));
|
||||
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
|
||||
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
|
||||
|
||||
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome")));
|
||||
|
||||
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
|
||||
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar"));
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
|
||||
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
|
||||
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players")));
|
||||
verification.tooFastReconnect = deserialize(fromList(messagesConfig.getStringList("verification.too-fast-reconnect")));
|
||||
verification.alreadyVerifying = deserialize(fromList(messagesConfig.getStringList("verification.already-verifying")));
|
||||
verification.alreadyQueued = deserialize(fromList(messagesConfig.getStringList("verification.already-queued")));
|
||||
verification.blacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted")));
|
||||
verification.invalidUsername = deserialize(fromList(messagesConfig.getStringList("verification.invalid-username")));
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol")));
|
||||
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online")));
|
||||
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
|
||||
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
|
||||
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal"));
|
||||
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack"));
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
|
||||
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title")));
|
||||
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle")));
|
||||
notifications.notificationChat = formatString(fromList(messagesConfig.getStringList("notifications.chat")));
|
||||
private @NotNull Component deserializeDisconnectMessage(final String path) {
|
||||
return MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList(path)),
|
||||
Placeholder.component("prefix", prefix),
|
||||
Placeholder.component("header", header),
|
||||
Placeholder.component("footer", footer),
|
||||
Placeholder.unparsed("support-url", supportUrl));
|
||||
}
|
||||
|
||||
private @NotNull URL getAsset(final String url, final @NotNull Language language) {
|
||||
@@ -328,33 +310,17 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
private static int clamp(final int v, final int max, final int min) {
|
||||
return Math.max(Math.min(v, min), max);
|
||||
final int output = Math.max(Math.min(v, min), max);
|
||||
if (output != v) {
|
||||
Sonar.get().getLogger().warn("Clamped configuration value {} to {}", v, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public String formatAddress(final InetAddress inetAddress) {
|
||||
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list) {
|
||||
return formatString(String.join("<newline>", list));
|
||||
}
|
||||
|
||||
private static @NotNull Component deserialize(final String legacy) {
|
||||
return MiniMessage.miniMessage().deserialize(legacy);
|
||||
}
|
||||
|
||||
private @NotNull String formatString(final @NotNull String str) {
|
||||
return str
|
||||
.replace("%prefix%", prefix == null ? "" : prefix)
|
||||
.replace("%support-url%", supportUrl == null ? "" : supportUrl)
|
||||
.replace("%header%", header == null ? "" : header)
|
||||
.replace("%footer%", footer == null ? "" : footer);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final Verbose verbose = new Verbose();
|
||||
@Getter
|
||||
private final Commands commands = new Commands();
|
||||
@Getter
|
||||
private final Queue queue = new Queue();
|
||||
@Getter
|
||||
@@ -363,15 +329,14 @@ public final class SonarConfiguration {
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
@Getter
|
||||
private final Notifications notifications = new Notifications();
|
||||
|
||||
@Getter
|
||||
private String prefix;
|
||||
private Component prefix;
|
||||
private String supportUrl;
|
||||
private Component header;
|
||||
private Component footer;
|
||||
@Getter
|
||||
private String noPermission;
|
||||
private String header, footer;
|
||||
private Component noPermission;
|
||||
private boolean logPlayerAddresses;
|
||||
@Getter
|
||||
private int maxOnlinePerIp;
|
||||
@@ -385,22 +350,8 @@ public final class SonarConfiguration {
|
||||
private int attackCooldownDelay;
|
||||
@Getter
|
||||
private Component tooManyOnlinePerIp;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Notifications {
|
||||
private Component notificationTitle;
|
||||
private Component notificationSubtitle;
|
||||
private String notificationChat;
|
||||
}
|
||||
private List<String> verboseAnimation;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -414,53 +365,42 @@ public final class SonarConfiguration {
|
||||
private Timing timing;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Timing {
|
||||
ALWAYS, DURING_ATTACK, NEVER
|
||||
ALWAYS("Always"),
|
||||
DURING_ATTACK("During Attack"),
|
||||
NEVER("Never");
|
||||
|
||||
private final String displayName;
|
||||
}
|
||||
|
||||
private final Map map = new Map();
|
||||
private final Gravity gravity = new Gravity();
|
||||
private final Vehicle vehicle = new Vehicle();
|
||||
private final Brand brand = new Brand();
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private Timing timing;
|
||||
private boolean randomizePositions;
|
||||
private boolean randomizeFontSize;
|
||||
private double distortionsFactorX;
|
||||
private double distortionsFactorY;
|
||||
private int randomLinesAmount;
|
||||
private int randomOvalsAmount;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
private String enterCodeActionBar;
|
||||
private List<String> fonts;
|
||||
private String alphabet;
|
||||
private InputStream backgroundImage;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Gravity {
|
||||
private boolean enabled;
|
||||
private boolean checkCollisions;
|
||||
private Gamemode gamemode;
|
||||
private boolean captchaOnFail;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
private Component youAreBeingChecked;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
// Keep this for backwards compatibility
|
||||
SPECTATOR(2);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
@Getter
|
||||
public static final class Vehicle {
|
||||
private boolean enabled;
|
||||
private int minimumPackets;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -470,29 +410,41 @@ public final class SonarConfiguration {
|
||||
private Pattern validRegex;
|
||||
}
|
||||
|
||||
private Gamemode gamemode;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
NOT_SET(-1),
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
public boolean isSurvivalOrAdventure() {
|
||||
return this == SURVIVAL || this == ADVENTURE;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkGeyser;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
private Pattern validNameRegex;
|
||||
private Pattern validLocaleRegex;
|
||||
private String connectLog;
|
||||
private String failedLog;
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int maxPing;
|
||||
private int readTimeout;
|
||||
private int writeTimeout;
|
||||
private int reconnectDelay;
|
||||
private int rememberTime;
|
||||
private int blacklistTime;
|
||||
private int blacklistThreshold;
|
||||
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
|
||||
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
|
||||
|
||||
private Component tooManyPlayers;
|
||||
private Component tooFastReconnect;
|
||||
private Component invalidUsername;
|
||||
private Component alreadyConnected;
|
||||
private Component verificationSuccess;
|
||||
private Component verificationFailed;
|
||||
private Component alreadyVerifying;
|
||||
@@ -502,54 +454,6 @@ public final class SonarConfiguration {
|
||||
private Component currentlyPreparing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Commands {
|
||||
private String incorrectCommandUsage;
|
||||
private String incorrectIpAddress;
|
||||
private String illegalIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
private String commandCoolDown;
|
||||
private String commandCoolDownLeft;
|
||||
|
||||
private String blacklistEmpty;
|
||||
private String blacklistAdd;
|
||||
private String blacklistAddWarning;
|
||||
private String blacklistDuplicate;
|
||||
private String blacklistNotFound;
|
||||
private String blacklistRemove;
|
||||
private String blacklistCleared;
|
||||
private String blacklistSize;
|
||||
|
||||
private String verifiedRemove;
|
||||
private String verifiedNotFound;
|
||||
private String verifiedCleared;
|
||||
private String verifiedSize;
|
||||
private String verifiedEmpty;
|
||||
private String verifiedBlocked;
|
||||
|
||||
private String statisticsHeader;
|
||||
private String unknownStatisticType;
|
||||
private String generalStatistics;
|
||||
private String memoryStatistics;
|
||||
private String networkStatistics;
|
||||
private String cpuStatistics;
|
||||
|
||||
private List<String> helpHeader;
|
||||
private String helpSubcommands;
|
||||
|
||||
private String verboseSubscribed;
|
||||
private String verboseUnsubscribed;
|
||||
|
||||
private String notificationsSubscribed;
|
||||
private String notificationsUnsubscribed;
|
||||
|
||||
private String reloading;
|
||||
private String reloaded;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Database {
|
||||
@@ -557,18 +461,38 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Type {
|
||||
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver"),
|
||||
NONE(null);
|
||||
MYSQL("MySQL", "jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("9.0.0")
|
||||
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
|
||||
.build()),
|
||||
MARIADB("MariaDB", "jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("org{}mariadb{}jdbc")
|
||||
.artifactId("mariadb-java-client")
|
||||
.version("3.4.1")
|
||||
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
|
||||
.build()),
|
||||
H2("H2", "jdbc:h2:file:%s", new H2DatabaseTypeAdapter(),
|
||||
Library.builder()
|
||||
.groupId("com{}h2database")
|
||||
.artifactId("h2")
|
||||
.version("2.2.220")
|
||||
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
|
||||
.build()),
|
||||
NONE("None", null, null, null);
|
||||
|
||||
private final String driverClassName;
|
||||
private final String displayName;
|
||||
private final String connectionString;
|
||||
private final DatabaseType databaseType;
|
||||
private final Library databaseDriver;
|
||||
@Setter
|
||||
private boolean downloaded;
|
||||
}
|
||||
|
||||
private Type type;
|
||||
private String host;
|
||||
private int port;
|
||||
private String name;
|
||||
private String username;
|
||||
private String password;
|
||||
private int maximumAge;
|
||||
}
|
||||
|
||||
|
||||
+105
-108
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.controller;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.j256.ormlite.dao.Dao;
|
||||
import com.j256.ormlite.dao.DaoManager;
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
|
||||
import com.j256.ormlite.stmt.QueryBuilder;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import com.j256.ormlite.table.TableUtils;
|
||||
@@ -27,14 +28,15 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.sql.*;
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
@@ -43,7 +45,6 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
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() {
|
||||
@@ -64,15 +65,17 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
};
|
||||
|
||||
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@Getter
|
||||
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
|
||||
|
||||
public VerifiedPlayerController() {
|
||||
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
// Cache selected database type, so we don't need to call Sonar.get() every time
|
||||
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
|
||||
cachedDatabaseType = database.getType();
|
||||
|
||||
// Don't establish a database connection if the type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
@@ -80,10 +83,41 @@ public final class VerifiedPlayerController {
|
||||
return;
|
||||
}
|
||||
|
||||
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
|
||||
this.connectionSource = connectionSource;
|
||||
// Make sure to only download the driver once
|
||||
if (!cachedDatabaseType.isDownloaded()) {
|
||||
LOGGER.info("Downloading {} driver version {}",
|
||||
cachedDatabaseType.getDatabaseType().getDatabaseName(),
|
||||
cachedDatabaseType.getDatabaseDriver().getVersion());
|
||||
// Download and load the driver for the current database
|
||||
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
|
||||
cachedDatabaseType.setDownloaded(true);
|
||||
}
|
||||
// Make sure to actually register the loader, so we can use it
|
||||
cachedDatabaseType.getDatabaseType().loadDriver();
|
||||
|
||||
// Create table
|
||||
try {
|
||||
final String jdbcURL;
|
||||
|
||||
// H2 has a different JDBC URL layout
|
||||
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
|
||||
final File file = new File(Sonar.get().getConfig().getPluginFolder(),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.filename"));
|
||||
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
|
||||
} else {
|
||||
// Normal JDBC URL layout for MySQL/MariaDB/...
|
||||
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.host"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getInt("database.port"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
|
||||
}
|
||||
|
||||
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
|
||||
cachedDatabaseType.getDatabaseType());
|
||||
|
||||
// Create database table
|
||||
try {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException exception) {
|
||||
@@ -98,83 +132,52 @@ public final class VerifiedPlayerController {
|
||||
// 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 {
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(database.getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Could not cache entries: {}", exception);
|
||||
LOGGER.error("Error initializing database: {}", exception);
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.error("Error setting up database: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error setting up database: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets up the database connection using the configured credentials
|
||||
* Closes the connection if instantiated
|
||||
*/
|
||||
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);
|
||||
public void close() {
|
||||
// The connection source will always be null if the database type is NONE.
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
connectionSource.close();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error("Error closing database: {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
|
||||
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();
|
||||
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);
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to clear old entries: {}", exception);
|
||||
LOGGER.info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +224,7 @@ public final class VerifiedPlayerController {
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
private void _remove(final @NotNull String inetAddress) {
|
||||
MAP.remove(inetAddress);
|
||||
cache.remove(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +249,7 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
try {
|
||||
dao.create(player);
|
||||
dao.createIfNotExists(player);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to add entry: {}", exception);
|
||||
}
|
||||
@@ -259,40 +262,53 @@ public final class VerifiedPlayerController {
|
||||
* @param player VerifiedPlayer model
|
||||
*/
|
||||
private void _add(final @NotNull VerifiedPlayer player) {
|
||||
MAP.computeIfAbsent(player.getInetAddress(), v -> new Vector<>())
|
||||
.add(player.getPlayerUUID());
|
||||
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
|
||||
.add(player.getPlayerUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of verified IP addresses
|
||||
*
|
||||
* @return Estimated size of the local cache
|
||||
*/
|
||||
public synchronized int estimatedSize() {
|
||||
return MAP.values().stream()
|
||||
public int estimatedSize() {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of verified IP addresses
|
||||
* and their respective collection of UUIDs
|
||||
*
|
||||
* @return Exact size of all total verified players
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized int exactSize() {
|
||||
return cache.values().stream()
|
||||
.mapToInt(Collection::size)
|
||||
.sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return List of UUIDs associated with one IP address
|
||||
* @return {@link java.util.Collection} of UUIDs associated with an IP address
|
||||
*/
|
||||
public Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
|
||||
return MAP.getOrDefault(inetAddress, Collections.emptyList());
|
||||
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
|
||||
return cache.getOrDefault(inetAddress, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the local cache, and then if the database type is set,
|
||||
* Clear the local cache, and, if the database type is set,
|
||||
* remove the table from the database.
|
||||
*/
|
||||
public void clearAll() {
|
||||
try {
|
||||
MAP.clear();
|
||||
public synchronized void clearAll() {
|
||||
cache.clear();
|
||||
|
||||
// Only update the column if the database type is not NONE
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
// Delete the entire table from the database, if necessary
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
try {
|
||||
dao.deleteBuilder().delete();
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,37 +318,18 @@ public final class VerifiedPlayerController {
|
||||
* @return Whether the local cache contains the IP and UUID
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
|
||||
final Collection<UUID> got = MAP.get(inetAddress);
|
||||
final Collection<UUID> got = cache.get(inetAddress);
|
||||
if (got != null) {
|
||||
return got.contains(uuid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
* @return Whether the local cache contains the IP and UUID
|
||||
* @see #has(String, UUID)
|
||||
*/
|
||||
public boolean has(final @NotNull InetAddress inetAddress, final @NotNull UUID uuid) {
|
||||
return has(inetAddress.toString(), uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
*/
|
||||
public boolean has(final @NotNull String inetAddress) {
|
||||
return MAP.containsKey(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
* @see #has(String)
|
||||
*/
|
||||
public boolean has(final @NotNull InetAddress inetAddress) {
|
||||
return has(inetAddress.toString());
|
||||
return cache.containsKey(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackTracker.AttackStatistics attackStatistics;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
|
||||
+6
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,25 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser user;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
private final @NotNull FallbackUser user;
|
||||
private final @NotNull String reason;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
|
||||
+1
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -28,7 +27,6 @@ import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -25,60 +25,62 @@ import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
// 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);
|
||||
// Map of all players connected to the server in general
|
||||
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
|
||||
// Map of all connected IP addresses (used for fast checking)
|
||||
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@Setter
|
||||
private Cache<InetAddress, Byte> blacklist;
|
||||
@Setter
|
||||
private long blacklistTime;
|
||||
@Setter
|
||||
private CaptchaGenerator captchaGenerator;
|
||||
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
private final @NotNull FallbackQueue queue = new FallbackQueue();
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
|
||||
|
||||
private final LoggerWrapper logger = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
SONAR.getLogger().info("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().info("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
SONAR.getLogger().warn("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().warn("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
SONAR.getLogger().error("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().error("[fallback] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
|
||||
}
|
||||
|
||||
public boolean shouldDoMapCaptcha() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
public boolean shouldPerformCaptcha() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
}
|
||||
|
||||
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
|
||||
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,8 +18,14 @@
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
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_BANDWIDTH = "sonar-fallback-bandwidth";
|
||||
String FALLBACK_INACTIVE_LISTENER = "sonar-inactive-listener";
|
||||
String FALLBACK_INBOUND_HANDLER = "sonar-inbound-handler";
|
||||
String FALLBACK_FRAME_DECODER = "sonar-frame-decoder";
|
||||
String FALLBACK_FRAME_ENCODER = "sonar-frame-encoder";
|
||||
String FALLBACK_TIMEOUT = "sonar-timeout";
|
||||
String FALLBACK_PACKET_HANDLER = "sonar-packet-handler";
|
||||
String FALLBACK_PACKET_ENCODER = "sonar-packet-encoder";
|
||||
String FALLBACK_PACKET_DECODER = "sonar-packet-decoder";
|
||||
String FALLBACK_TAIL_EXCEPTIONS = "sonar-exception-tail";
|
||||
String FALLBACK_BANDWIDTH = "sonar-bandwidth-counter";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -26,51 +26,33 @@ import java.net.InetAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackQueue {
|
||||
static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
|
||||
// Fixed thread pool executor for all new verifications
|
||||
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
|
||||
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
|
||||
|
||||
// Runnable task executed for polling the queue
|
||||
private final Runnable pollTask = () -> {
|
||||
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
|
||||
128, 0.75f, Runtime.getRuntime().availableProcessors());
|
||||
|
||||
public void poll() {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// We need to be cautious here since we don't want any concurrency issues.
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
// Iterate through the map and process up to maxQueuePolls entries
|
||||
// We need to be cautious here since we don't want any concurrency issues
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = players.entrySet().iterator();
|
||||
while (iterator.hasNext() && index++ < maxQueuePolls) {
|
||||
// Run the cached runnable
|
||||
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
|
||||
entry.getValue().run();
|
||||
// Remove runnable from iterator
|
||||
QUEUE_EXECUTOR.execute(entry.getValue());
|
||||
// Remove runnable from the map
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// 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-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,35 +27,39 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackRatelimiter {
|
||||
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
private Cache<InetAddress, Byte> expiringCache;
|
||||
private long timeout;
|
||||
private Cache<InetAddress, Long> attemptCache;
|
||||
@Getter
|
||||
private Cache<InetAddress, Integer> failCountCache;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Attempts to rate-limit the client
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @return Whether the player is allowed to verify
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!expiringCache.asMap().containsKey(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress, (byte) 0);
|
||||
return true;
|
||||
final long expectedTimestamp = System.currentTimeMillis() + timeout;
|
||||
// Check if the time since the last join has not exceeded the timeout
|
||||
// The idea for this check was Taken from Velocity
|
||||
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
|
||||
return expectedTimestamp == last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player is failing verifications too often
|
||||
* Increments the number of times this user has failed the verification
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @param count Count of previously failed verifications
|
||||
*/
|
||||
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
|
||||
// Make sure to remove the old values from the cache
|
||||
if (count > 0) {
|
||||
failCountCache.invalidate(inetAddress);
|
||||
}
|
||||
// Deny the connection attempt
|
||||
return false;
|
||||
failCountCache.put(inetAddress, count + 1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,52 +17,61 @@
|
||||
|
||||
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.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull Channel getChannel();
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
@NotNull
|
||||
ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
|
||||
@NotNull
|
||||
ProtocolVersion getProtocolVersion();
|
||||
|
||||
@NotNull
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
@NotNull
|
||||
UUID getOfflineUuid();
|
||||
|
||||
boolean isReceivedClientSettings();
|
||||
|
||||
void setReceivedClientSettings(final boolean receivedClientSettings);
|
||||
|
||||
boolean isReceivedPluginMessage();
|
||||
|
||||
void setReceivedPluginMessage(final boolean receivedPluginMessage);
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom Disconnect packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
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
|
||||
* @param username Username of the player
|
||||
* @param offlineUuid UUID of the player
|
||||
*/
|
||||
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);
|
||||
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
@@ -89,40 +98,6 @@ public interface FallbackUser {
|
||||
}
|
||||
}
|
||||
|
||||
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Increments the number of times this user (by IP address) has failed the verification
|
||||
*/
|
||||
default void incrementFails() {
|
||||
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
|
||||
final int failCount = getFailedCount();
|
||||
// Make sure to remove the old values from the cache
|
||||
if (failCount > 0) {
|
||||
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
|
||||
}
|
||||
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of times this user (by IP address) has failed the verification
|
||||
* @apiNote Returns 0 if no key/value is present in the cache
|
||||
*/
|
||||
default int getFailedCount() {
|
||||
// Make sure to clean up the cache before we try to get cached values
|
||||
GLOBAL_FAIL_COUNT.cleanUp();
|
||||
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all previously failed verifications
|
||||
*/
|
||||
default void invalidateFails() {
|
||||
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
* and caches them in FAILED_VERIFICATIONS.
|
||||
@@ -130,55 +105,8 @@ public interface FallbackUser {
|
||||
* the player will be temporarily denied from verifying.
|
||||
*
|
||||
* @param reason Reason for failing the verification
|
||||
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
|
||||
* will not be thrown if no reason is given
|
||||
*/
|
||||
default void fail(final @Nullable String reason) {
|
||||
if (getChannel().isActive()) {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
// Only log the failed message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Statistics.FAILED_VERIFICATIONS.increment();
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
|
||||
final int failCount = getFailedCount() + 1;
|
||||
// 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));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
void fail(final @NotNull String reason);
|
||||
}
|
||||
+5
-10
@@ -15,16 +15,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.block;
|
||||
package xyz.jonesdev.sonar.api.fallback.captcha;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public final class BlockPosition {
|
||||
private final int x, y, z;
|
||||
private final int chunkX, chunkZ;
|
||||
@FunctionalInterface
|
||||
public interface CaptchaGenerator {
|
||||
BufferedImage createImage(final char[] answer);
|
||||
}
|
||||
+11
-30
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -20,13 +20,13 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ProtocolVersion {
|
||||
UNKNOWN(-1),
|
||||
LEGACY(-2),
|
||||
MINECRAFT_1_7_2(4),
|
||||
MINECRAFT_1_7_6(5),
|
||||
MINECRAFT_1_8(47),
|
||||
@@ -66,38 +66,24 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765);
|
||||
MINECRAFT_1_20_3(765),
|
||||
MINECRAFT_1_20_5(766),
|
||||
MINECRAFT_1_21(767);
|
||||
|
||||
private final int protocol;
|
||||
|
||||
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
|
||||
public static final ProtocolVersion LATEST_VERSION;
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
|
||||
|
||||
static {
|
||||
{
|
||||
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
|
||||
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
|
||||
|
||||
for (final ProtocolVersion version : values()) {
|
||||
versions.putIfAbsent(version.protocol, version);
|
||||
}
|
||||
|
||||
ID_TO_PROTOCOL_CONSTANT = versions;
|
||||
for (final ProtocolVersion version : values()) {
|
||||
versions.putIfAbsent(version.protocol, version);
|
||||
}
|
||||
|
||||
{
|
||||
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
|
||||
|
||||
for (final ProtocolVersion value : values()) {
|
||||
if (!value.isUnknown() && !value.isLegacy()) {
|
||||
versions.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
|
||||
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
|
||||
}
|
||||
ID_TO_PROTOCOL_CONSTANT = versions;
|
||||
LATEST_VERSION = values()[values().length - 1];
|
||||
}
|
||||
|
||||
public static ProtocolVersion fromId(final int protocol) {
|
||||
@@ -111,9 +97,4 @@ public enum ProtocolVersion {
|
||||
public boolean isUnknown() {
|
||||
return this == UNKNOWN;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isLegacy() {
|
||||
return this == LEGACY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.jvm;
|
||||
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@UtilityClass
|
||||
public class JVMProcessInformation {
|
||||
private final Runtime RUNTIME = Runtime.getRuntime();
|
||||
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
|
||||
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
|
||||
|
||||
public String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
return size + " B";
|
||||
}
|
||||
// https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
|
||||
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
|
||||
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Number of available processors for the JVM
|
||||
*/
|
||||
public int getVirtualCores() {
|
||||
return RUNTIME.availableProcessors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current process CPU usage in percent
|
||||
*/
|
||||
public double getProcessCPUUsage() {
|
||||
return MX.getProcessCpuLoad() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current per-core process CPU usage in percent
|
||||
*/
|
||||
public double getAverageProcessCPUUsage() {
|
||||
return getProcessCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current system CPU usage in percent
|
||||
*/
|
||||
public double getSystemCPUUsage() {
|
||||
return MX.getSystemCpuLoad() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current per-core system CPU usage in percent
|
||||
*/
|
||||
public double getAverageSystemCPUUsage() {
|
||||
return getSystemCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM will attempt to use
|
||||
*/
|
||||
public long getMaxMemory() {
|
||||
return RUNTIME.maxMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) available to the host machine
|
||||
*/
|
||||
public long getTotalMemory() {
|
||||
return RUNTIME.totalMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM can allocate
|
||||
*/
|
||||
public long getFreeMemory() {
|
||||
return RUNTIME.freeMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM is using
|
||||
*/
|
||||
public long getUsedMemory() {
|
||||
return getTotalMemory() - getFreeMemory();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -23,7 +23,9 @@ import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -50,7 +52,7 @@ public final class VerifiedPlayer {
|
||||
uniqueIndexName = "ip_address_player_uuid_idx",
|
||||
width = 36
|
||||
)
|
||||
private UUID playerUUID;
|
||||
private UUID playerUuid;
|
||||
|
||||
@DatabaseField(
|
||||
columnName = "timestamp",
|
||||
@@ -58,9 +60,17 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
|
||||
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this(inetAddress.toString(), playerUuid, timestamp);
|
||||
}
|
||||
|
||||
public VerifiedPlayer(final @NotNull String inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUUID = playerUUID;
|
||||
this.playerUuid = playerUuid;
|
||||
this.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -15,16 +15,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import com.j256.ormlite.jdbc.db.H2DatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class MapInfo {
|
||||
private final String answer;
|
||||
private final int columns, rows;
|
||||
private final int x, y;
|
||||
private final byte[] buffer;
|
||||
public final class H2DatabaseTypeAdapter extends H2DatabaseType {
|
||||
|
||||
// We need to override the default driver class name
|
||||
// to use the custom relocated H2 driver
|
||||
@Override
|
||||
protected String @NotNull [] getDriverClassNames() {
|
||||
return new String[]{"xyz.jonesdev.sonar.libs.h2.Driver"};
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -15,18 +15,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.command;
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
|
||||
public final class VelocityInvocationSource extends InvocationSource {
|
||||
public VelocityInvocationSource(final @NotNull CommandSource sender) {
|
||||
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
|
||||
sender,
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
|
||||
|
||||
// We need to override the default driver class name
|
||||
// to use the custom relocated MariaDB driver
|
||||
@Override
|
||||
protected String @NotNull [] getDriverClassNames() {
|
||||
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
|
||||
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
|
||||
|
||||
// We need to override the default driver class name
|
||||
// to use the custom relocated MySQL driver
|
||||
@Override
|
||||
protected String @NotNull [] getDriverClassNames() {
|
||||
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.profiler;
|
||||
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
|
||||
public interface JVMProfiler {
|
||||
Runtime RUNTIME = Runtime.getRuntime();
|
||||
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
|
||||
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
|
||||
|
||||
default String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
return size + " B";
|
||||
}
|
||||
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
return String.format("%.1f %sB", formattedSize, unit);
|
||||
}
|
||||
|
||||
default int getVirtualCores() {
|
||||
return RUNTIME.availableProcessors();
|
||||
}
|
||||
|
||||
default double getProcessCPUUsage() {
|
||||
return OPERATING_SYSTEM_MX_BEAN.getProcessCpuLoad() * 100;
|
||||
}
|
||||
|
||||
default double getAverageProcessCPUUsage() {
|
||||
return getProcessCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
default double getSystemCPUUsage() {
|
||||
return OPERATING_SYSTEM_MX_BEAN.getSystemCpuLoad() * 100;
|
||||
}
|
||||
|
||||
default double getAverageSystemCPUUsage() {
|
||||
return getSystemCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
default double getSystemLoadAverage() {
|
||||
return OPERATING_SYSTEM_MX_BEAN.getSystemLoadAverage() * 100;
|
||||
}
|
||||
|
||||
default long getMaxMemory() {
|
||||
return RUNTIME.maxMemory();
|
||||
}
|
||||
|
||||
default long getTotalMemory() {
|
||||
return RUNTIME.totalMemory();
|
||||
}
|
||||
|
||||
default long getFreeMemory() {
|
||||
return RUNTIME.freeMemory();
|
||||
}
|
||||
|
||||
default long getUsedMemory() {
|
||||
return getTotalMemory() - getFreeMemory();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface SonarStatistics {
|
||||
|
||||
/**
|
||||
* @return Number of connection establishments per second
|
||||
*/
|
||||
long getConnectionsPerSecond();
|
||||
|
||||
/**
|
||||
* @return Number of logins per second
|
||||
*/
|
||||
long getLoginsPerSecond();
|
||||
|
||||
/**
|
||||
* @return Currently incoming bytes per second
|
||||
*/
|
||||
long getCurrentIncomingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Currently outgoing bytes per second
|
||||
*/
|
||||
long getCurrentOutgoingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Total incoming bytes per second
|
||||
*/
|
||||
long getTotalIncomingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Total outgoing bytes per second
|
||||
*/
|
||||
long getTotalOutgoingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getCurrentIncomingBandwidth} but formatted
|
||||
*/
|
||||
String getCurrentIncomingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getCurrentOutgoingBandwidth} but formatted
|
||||
*/
|
||||
String getCurrentOutgoingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getTotalIncomingBandwidth} but formatted
|
||||
*/
|
||||
String getTotalIncomingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getTotalOutgoingBandwidth} but formatted
|
||||
*/
|
||||
String getTotalOutgoingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Total number of players who logged into the server
|
||||
*/
|
||||
int getTotalPlayersJoined();
|
||||
|
||||
/**
|
||||
* @return Total number of players who tried to verify
|
||||
*/
|
||||
int getTotalPlayersVerified();
|
||||
|
||||
/**
|
||||
* @return Total number of players who passed the verification
|
||||
*/
|
||||
int getTotalSuccessfulVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who failed the verification
|
||||
*/
|
||||
int getTotalFailedVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who passed the verification
|
||||
*/
|
||||
long getCurrentAttemptedVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who tried verifying
|
||||
*/
|
||||
int getTotalAttemptedVerifications();
|
||||
|
||||
/**
|
||||
* @return Current number of IP addresses that are blacklisted
|
||||
*/
|
||||
long getCurrentBlacklistSize();
|
||||
|
||||
/**
|
||||
* @return Total number of cases where an IP was blacklisted
|
||||
* @apiNote This does not describe the current number of blacklisted IPs
|
||||
*/
|
||||
long getTotalBlacklistSize();
|
||||
|
||||
@ApiStatus.Internal
|
||||
void cleanUpCache();
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
public enum Statistics {
|
||||
TOTAL_TRAFFIC,
|
||||
REAL_TRAFFIC,
|
||||
FAILED_VERIFICATIONS;
|
||||
|
||||
private int val = -1;
|
||||
|
||||
/**
|
||||
* Increments the current value by 1 with the default value of 0
|
||||
*
|
||||
* @see #get(int)
|
||||
* @see #set(int)
|
||||
*/
|
||||
public void increment() {
|
||||
increment(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the current value by 1 with a custom default value
|
||||
*
|
||||
* @see #get(int)
|
||||
* @see #set(int)
|
||||
*/
|
||||
public void increment(final int def) {
|
||||
set(get(def) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current value with the default value of 0
|
||||
* @see #get(int)
|
||||
*/
|
||||
public int get() {
|
||||
return get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current value with a custom default value
|
||||
*/
|
||||
public int get(final int def) {
|
||||
return val == -1 ? def : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current value to a new value
|
||||
*/
|
||||
public void set(final int value) {
|
||||
val = value;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,13 +18,12 @@
|
||||
package xyz.jonesdev.sonar.api.timer;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
@Getter
|
||||
public final class SystemTimer {
|
||||
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
|
||||
private long start = System.currentTimeMillis();
|
||||
|
||||
@@ -32,18 +31,20 @@ public final class SystemTimer {
|
||||
start = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Time between the current timestamp and the start timestamp
|
||||
*/
|
||||
public long delay() {
|
||||
return System.currentTimeMillis() - start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public boolean elapsed(final long amount) {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
public @NotNull String formattedDelay() {
|
||||
return FORMATTER.format(delay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
|
||||
@@ -21,59 +21,69 @@ 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.text.minimessage.tag.resolver.Placeholder;
|
||||
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 java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
public final class Notification extends Observable {
|
||||
|
||||
public void sendAttackNotification() {
|
||||
// No need to do anything if we have no subscribers
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are 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()));
|
||||
|
||||
// Prepare the chat notification
|
||||
final Component notificationTitle = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("notifications.title"));
|
||||
final Component notificationSubtitle = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("notifications.subtitle"));
|
||||
final Title title = Title.title(notificationTitle, notificationSubtitle);
|
||||
|
||||
final List<String> chatNotification = Sonar.get().getConfig().getMessagesConfig().getStringList("notifications.chat");
|
||||
final Component[] chatNotificationComponents = new Component[chatNotification.size()];
|
||||
|
||||
for (int i = 0; i < chatNotificationComponents.length; i++) {
|
||||
chatNotificationComponents[i] = MiniMessage.miniMessage().deserialize(chatNotification.get(i),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
|
||||
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
|
||||
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
|
||||
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
|
||||
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
|
||||
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
|
||||
}
|
||||
|
||||
// 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);
|
||||
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
|
||||
// Send the message to all online subscribers
|
||||
for (Component component : chatNotificationComponents) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
// Send the title to all online subscribers
|
||||
audience.showTitle(title);
|
||||
audience.sendMessage(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,32 +17,38 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
public interface Observable {
|
||||
Collection<String> getSubscribers();
|
||||
@Getter
|
||||
public abstract class Observable {
|
||||
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public abstract void observe();
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @param uuid UUID of the player
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
default boolean isSubscribed(final @NotNull String name) {
|
||||
return getSubscribers().contains(name);
|
||||
public final boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
return getSubscribers().contains(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void subscribe(final String name) {
|
||||
getSubscribers().add(name);
|
||||
public final void subscribe(final UUID uuid) {
|
||||
getSubscribers().add(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void unsubscribe(final String name) {
|
||||
getSubscribers().remove(name);
|
||||
public final void unsubscribe(final UUID uuid) {
|
||||
getSubscribers().remove(uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,82 +18,72 @@
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.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.Vector;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements Observable, JVMProfiler, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
public final class Verbose extends Observable {
|
||||
private int animationIndex;
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Clean up all counters
|
||||
LOGINS_PER_SECOND.cleanUp();
|
||||
CONNECTIONS_PER_SECOND.cleanUp();
|
||||
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the action bar verbose
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackTimer = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
final String attackDuration = attackTimer == null ? "00:00" : DATE_FORMATTER.format(attackTimer.delay());
|
||||
|
||||
final Component actionBarComponent = MiniMessage.miniMessage().deserialize(attackTimer == null
|
||||
? Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.normal")
|
||||
: Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.attack"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("attack-duration", attackDuration),
|
||||
Placeholder.unparsed("animation", nextAnimation()),
|
||||
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
|
||||
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
|
||||
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
|
||||
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
|
||||
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
|
||||
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
|
||||
|
||||
// Send the action bar to all online players
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = Sonar.AUDIENCES.get(subscriber);
|
||||
for (final UUID subscriber : subscribers) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
|
||||
// Send the action bar to all online subscribers
|
||||
audience.sendActionBar(actionBarComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
return MiniMessage.miniMessage().deserialize((attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.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("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
|
||||
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
|
||||
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
|
||||
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
|
||||
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl())
|
||||
.replace("%used-memory%", formatMemory(getUsedMemory()))
|
||||
.replace("%free-memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%total-memory%", formatMemory(getTotalMemory()))
|
||||
.replace("%max-memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final var animations = Sonar.get().getConfig().getVerboseAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.webhook;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -32,12 +34,18 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class DiscordWebhook {
|
||||
private final String url;
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Asynchronously posts a Discord webhook via HTTPS
|
||||
*/
|
||||
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
|
||||
HTTP_REQUEST_SERVICE.execute(() -> {
|
||||
try {
|
||||
@@ -47,9 +55,11 @@ public final class DiscordWebhook {
|
||||
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
// Check if the connection was a success
|
||||
if (urlConnection.getResponseCode() != 204) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
|
||||
// Discord replies with HTTP_NO_CONTENT (204) if the request is successful
|
||||
final int code = urlConnection.getResponseCode();
|
||||
if (code != HTTP_NO_CONTENT) {
|
||||
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
|
||||
@@ -57,7 +67,7 @@ public final class DiscordWebhook {
|
||||
});
|
||||
}
|
||||
|
||||
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
private static String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
final String content = Sonar.get().getConfig().getWebhook().getContent();
|
||||
final String username = Sonar.get().getConfig().getWebhook().getUsername();
|
||||
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
|
||||
@@ -73,7 +83,7 @@ public final class DiscordWebhook {
|
||||
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
|
||||
|
||||
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
|
||||
return Sonar.GENERAL_GSON.toJson(webhook);
|
||||
return GSON.toJson(webhook);
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
|
||||
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
|
||||
alias(libs.plugins.pluginyml.bukkit) apply true
|
||||
}
|
||||
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
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")
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
|
||||
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
|
||||
apiVersion = "1.13" // ignore legacy plugin warning
|
||||
foliaSupported = true
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
@@ -28,20 +28,23 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
|
||||
compileOnly(rootProject.libs.spigot)
|
||||
testCompileOnly(rootProject.libs.spigot)
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
implementation(rootProject.libs.adventure.platform.bukkit) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bukkit)
|
||||
implementation(rootProject.libs.libby.bukkit)
|
||||
}
|
||||
|
||||
// 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")
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -19,28 +19,28 @@ package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import com.alessiodp.libby.BukkitLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.bukkit.fallback.FallbackBukkitInjector;
|
||||
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Getter
|
||||
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, new BukkitLibraryManager(
|
||||
plugin, plugin.getDataFolder().getName()),
|
||||
plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +48,19 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
*/
|
||||
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
|
||||
|
||||
@Override
|
||||
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
|
||||
if (uniqueId == null) {
|
||||
return null;
|
||||
}
|
||||
return bukkitAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Audience sender(final @NotNull Object object) {
|
||||
return bukkitAudiences.sender((CommandSender) object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -72,28 +85,44 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
}
|
||||
};
|
||||
|
||||
private Metrics metrics;
|
||||
|
||||
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Add charts for some configuration options
|
||||
metrics.addCustomChart(new SimplePie("verification",
|
||||
() -> getConfig().getVerification().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("captcha",
|
||||
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("language",
|
||||
() -> getConfig().getLanguage().getName()));
|
||||
metrics.addCustomChart(new SimplePie("database_type",
|
||||
() -> getConfig().getDatabase().getType().getDisplayName()));
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
// Try to inject into the server
|
||||
if (FallbackBukkitInjector.isLateBindEnabled()) {
|
||||
getPlugin().getServer().getScheduler().runTask(getPlugin(), FallbackBukkitInjector::inject);
|
||||
} else {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin());
|
||||
}
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Bandwidth::reset, 20L, 20L);
|
||||
// Let the injector know that the plugin has been enabled
|
||||
INITIALIZE_LISTENER.complete(null);
|
||||
}
|
||||
|
||||
// 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);
|
||||
@Override
|
||||
public void disable() {
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,10 +18,19 @@
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
|
||||
|
||||
public final class SonarBukkitPlugin extends JavaPlugin {
|
||||
private SonarBukkit bootstrap;
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
// Inject early if late-bind is disabled
|
||||
if (!FallbackBukkitInjector.isLateBindEnabled()) {
|
||||
FallbackBukkitInjector.inject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
bootstrap = new SonarBukkit(this);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class BukkitInvocationSource extends InvocationSource {
|
||||
public BukkitInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(),
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -21,7 +21,9 @@ import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
|
||||
@@ -37,7 +39,10 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
final String label,
|
||||
final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
|
||||
Sonar.get().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.fallback;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum BukkitServerVersion {
|
||||
MINECRAFT_1_7_10,
|
||||
MINECRAFT_1_8,
|
||||
MINECRAFT_1_8_3,
|
||||
MINECRAFT_1_8_8,
|
||||
MINECRAFT_1_9,
|
||||
MINECRAFT_1_9_2,
|
||||
MINECRAFT_1_9_4,
|
||||
MINECRAFT_1_10,
|
||||
MINECRAFT_1_10_1,
|
||||
MINECRAFT_1_10_2,
|
||||
MINECRAFT_1_11,
|
||||
MINECRAFT_1_11_2,
|
||||
MINECRAFT_1_12,
|
||||
MINECRAFT_1_12_1,
|
||||
MINECRAFT_1_12_2,
|
||||
MINECRAFT_1_13,
|
||||
MINECRAFT_1_13_1,
|
||||
MINECRAFT_1_13_2,
|
||||
MINECRAFT_1_14,
|
||||
MINECRAFT_1_14_1,
|
||||
MINECRAFT_1_14_2,
|
||||
MINECRAFT_1_14_3,
|
||||
MINECRAFT_1_14_4,
|
||||
MINECRAFT_1_15,
|
||||
MINECRAFT_1_15_1,
|
||||
MINECRAFT_1_15_2,
|
||||
MINECRAFT_1_16,
|
||||
MINECRAFT_1_16_1,
|
||||
MINECRAFT_1_16_2,
|
||||
MINECRAFT_1_16_3,
|
||||
MINECRAFT_1_16_4,
|
||||
MINECRAFT_1_16_5,
|
||||
MINECRAFT_1_17,
|
||||
MINECRAFT_1_17_1,
|
||||
MINECRAFT_1_18,
|
||||
MINECRAFT_1_18_1,
|
||||
MINECRAFT_1_18_2,
|
||||
MINECRAFT_1_19,
|
||||
MINECRAFT_1_19_1,
|
||||
MINECRAFT_1_19_2,
|
||||
MINECRAFT_1_19_3,
|
||||
MINECRAFT_1_19_4,
|
||||
MINECRAFT_1_20,
|
||||
MINECRAFT_1_20_1,
|
||||
MINECRAFT_1_20_2,
|
||||
MINECRAFT_1_20_3,
|
||||
MINECRAFT_1_20_4,
|
||||
MINECRAFT_1_20_5,
|
||||
MINECRAFT_1_20_6,
|
||||
MINECRAFT_1_21;
|
||||
|
||||
static final BukkitServerVersion[] REVERSED_VALUES;
|
||||
|
||||
static {
|
||||
REVERSED_VALUES = values().clone();
|
||||
|
||||
final int size = REVERSED_VALUES.length;
|
||||
final int half = size / 2;
|
||||
|
||||
for (int i = 0; i < half; i++) {
|
||||
final BukkitServerVersion temp = REVERSED_VALUES[i];
|
||||
REVERSED_VALUES[i] = REVERSED_VALUES[size - 1 - i];
|
||||
REVERSED_VALUES[size - 1 - i] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
private final String release;
|
||||
|
||||
BukkitServerVersion() {
|
||||
this.release = name().substring(10).replace("_", ".");
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package xyz.jonesdev.sonar.bukkit.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter {
|
||||
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
|
||||
for (final ChannelInboundHandler handler : handlers) {
|
||||
try {
|
||||
handler.channelInactive(proxy);
|
||||
} catch (Exception exception) {
|
||||
handler.exceptionCaught(proxy, exception);
|
||||
}
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
public void add(ChannelHandler handler) {
|
||||
if (handler instanceof ChannelInboundHandler) {
|
||||
handlers.add((ChannelInboundHandler) handler);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class ProxiedChannelHandlerContext implements ChannelHandlerContext {
|
||||
private final ChannelHandlerContext ctx;
|
||||
|
||||
// Overwrite to prevent call this method on next handler
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelInactive() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireExceptionCaught(Throwable throwable) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Channel channel() {
|
||||
return ctx.channel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventExecutor executor() {
|
||||
return ctx.executor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return ctx.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandler handler() {
|
||||
return ctx.handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemoved() {
|
||||
return ctx.isRemoved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelRegistered() {
|
||||
return ctx.fireChannelRegistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelUnregistered() {
|
||||
return ctx.fireChannelUnregistered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelActive() {
|
||||
return ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireUserEventTriggered(Object o) {
|
||||
return ctx.fireUserEventTriggered(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelRead(Object o) {
|
||||
return ctx.fireChannelRead(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelReadComplete() {
|
||||
return ctx.fireChannelReadComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext fireChannelWritabilityChanged() {
|
||||
return ctx.fireChannelWritabilityChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture bind(SocketAddress socketAddress) {
|
||||
return ctx.bind(socketAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress) {
|
||||
return ctx.connect(socketAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
|
||||
return ctx.connect(socketAddress, socketAddress1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture disconnect() {
|
||||
return ctx.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture close() {
|
||||
return ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture deregister() {
|
||||
return ctx.deregister();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
|
||||
return ctx.bind(socketAddress, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
|
||||
return ctx.connect(socketAddress, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
|
||||
return ctx.connect(socketAddress, socketAddress1, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture disconnect(ChannelPromise channelPromise) {
|
||||
return ctx.disconnect(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture close(ChannelPromise channelPromise) {
|
||||
return ctx.close(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture deregister(ChannelPromise channelPromise) {
|
||||
return ctx.deregister(channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext read() {
|
||||
return ctx.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture write(Object o) {
|
||||
return ctx.write(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
|
||||
return ctx.write(o, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelHandlerContext flush() {
|
||||
return ctx.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
|
||||
return ctx.writeAndFlush(o, channelPromise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture writeAndFlush(Object o) {
|
||||
return ctx.writeAndFlush(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPromise newPromise() {
|
||||
return ctx.newPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelProgressivePromise newProgressivePromise() {
|
||||
return ctx.newProgressivePromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture newSucceededFuture() {
|
||||
return ctx.newSucceededFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelFuture newFailedFuture(Throwable throwable) {
|
||||
return ctx.newFailedFuture(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPromise voidPromise() {
|
||||
return ctx.voidPromise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelPipeline pipeline() {
|
||||
return ctx.pipeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufAllocator alloc() {
|
||||
return ctx.alloc();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
|
||||
return ctx.attr(attributeKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
|
||||
return ctx.hasAttr(attributeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
|
||||
|
||||
FallbackBukkitInboundHandler() {
|
||||
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
|
||||
|
||||
channelRemovalListener = (pipeline, name, handler) -> {
|
||||
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
|
||||
|
||||
if (inactiveListener != null) {
|
||||
inactiveListener.add(handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
// Reset the reader index, so we can read the content
|
||||
// Make sure to store the original reader index
|
||||
final int originalReaderIndex = byteBuf.readerIndex();
|
||||
byteBuf.readerIndex(0);
|
||||
|
||||
// Don't continue if the ByteBuf isn't readable or the channel is closed
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// Skip the packet if it's not registered within Sonar's packet registry
|
||||
if (packet == null) {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to decode the packet for the given protocol version
|
||||
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
|
||||
} catch (Throwable throwable) {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
throw new CorruptedFrameException("Failed to decode packet");
|
||||
}
|
||||
|
||||
// Check if the packet still has bytes left after we decoded it
|
||||
if (byteBuf.isReadable()) {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
throw new CorruptedFrameException("Could not read packet to end");
|
||||
}
|
||||
|
||||
// Useful resources:
|
||||
// https://wiki.vg/Protocol#Handshaking
|
||||
// https://wiki.vg/Protocol#Login
|
||||
if (packet instanceof HandshakePacket) {
|
||||
final HandshakePacket handshake = (HandshakePacket) packet;
|
||||
switch (handshake.getIntent()) {
|
||||
case STATUS:
|
||||
// We don't care about server pings or transfers; remove the handler
|
||||
ctx.channel().pipeline().remove(this);
|
||||
break;
|
||||
case LOGIN:
|
||||
case TRANSFER:
|
||||
// Let the actual handler know about the handshake packet
|
||||
handleHandshake(ctx.channel(), handshake.getHostname(), handshake.getProtocolVersionId());
|
||||
// Be ready for the next packet (which is supposed to be a login packet)
|
||||
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
|
||||
break;
|
||||
default:
|
||||
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
|
||||
}
|
||||
} else if (packet instanceof LoginStartPacket) {
|
||||
final LoginStartPacket loginStart = (LoginStartPacket) packet;
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
|
||||
// We've done our job - deject this pipeline
|
||||
ctx.channel().pipeline().remove(this);
|
||||
// Let Sonar process the login packet
|
||||
handleLogin(ctx.channel(), ctx, () -> {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
ctx.fireChannelRead(byteBuf.retain());
|
||||
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
|
||||
if (inboundHandler != null) {
|
||||
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
|
||||
}
|
||||
}, loginStart.getUsername(), socketAddress);
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
ctx.fireChannelRead(byteBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
|
||||
// Check out these links if you want to see some more magic
|
||||
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
|
||||
// https://github.com/dmulloy2/ProtocolLib/blob/master/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java
|
||||
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/handlers/BukkitChannelInitializer.java
|
||||
@UtilityClass
|
||||
public class FallbackBukkitInjector {
|
||||
private final String LEGACY_NMS_PACKAGE;
|
||||
private final String OBC_PACKAGE;
|
||||
|
||||
private final BukkitServerVersion SERVER_VERSION;
|
||||
private final boolean USES_LEGACY_PACKAGING, OBFUSCATED;
|
||||
|
||||
private final Class<?> MINECRAFT_SERVER_CLASS;
|
||||
private final Class<?> CRAFTBUKKIT_SERVER_CLASS;
|
||||
private final Class<?> SERVER_CONNECTION_CLASS;
|
||||
private final Object SERVER_INSTANCE;
|
||||
|
||||
@Getter
|
||||
private boolean lateBindEnabled;
|
||||
|
||||
static {
|
||||
SERVER_VERSION = resolveServerVersion();
|
||||
USES_LEGACY_PACKAGING = SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_17) < 0;
|
||||
|
||||
try {
|
||||
final String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
String modifiedPackageName;
|
||||
if (SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_20_5) >= 0) {
|
||||
// Example: org.bukkit.craftbukkit
|
||||
modifiedPackageName = bukkitPackage.split("\\.")[2];
|
||||
} else {
|
||||
// Example: org.bukkit.craftbukkit.v1_16_R3
|
||||
modifiedPackageName = bukkitPackage.split("\\.")[3];
|
||||
}
|
||||
|
||||
LEGACY_NMS_PACKAGE = "net.minecraft.server." + modifiedPackageName + ".";
|
||||
OBC_PACKAGE = bukkitPackage + ".";
|
||||
|
||||
OBFUSCATED = isObfuscated();
|
||||
// Minecraft classes
|
||||
MINECRAFT_SERVER_CLASS = getNMSClass("server.MinecraftServer", "MinecraftServer");
|
||||
SERVER_CONNECTION_CLASS = getNMSClass(OBFUSCATED ? "server.network.ServerConnection" : "server.network.ServerConnectionListener", "ServerConnection");
|
||||
// CraftBukkit classes
|
||||
CRAFTBUKKIT_SERVER_CLASS = getOBCClass("CraftServer");
|
||||
|
||||
Object minecraftServerInstance;
|
||||
try {
|
||||
// 1.20.5+
|
||||
minecraftServerInstance = getFieldAt(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(null);
|
||||
} catch (Exception exception) {
|
||||
minecraftServerInstance = getFieldAt(CRAFTBUKKIT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(Bukkit.getServer());
|
||||
}
|
||||
SERVER_INSTANCE = minecraftServerInstance;
|
||||
|
||||
checkLateBind();
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLateBind() {
|
||||
try {
|
||||
try {
|
||||
Class.forName("org.bukkit.event.server.ServerLoadEvent");
|
||||
// late-bind does not exist on these versions, ignore this case
|
||||
return;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
}
|
||||
|
||||
final Class<?> spigotConfiguration = Class.forName("org.spigotmc.SpigotConfig");
|
||||
final Method initFieldMethod = spigotConfiguration.getDeclaredMethod("lateBind");
|
||||
initFieldMethod.setAccessible(true);
|
||||
initFieldMethod.invoke(null);
|
||||
|
||||
if ((boolean) spigotConfiguration.getField("lateBind").get(null)) {
|
||||
lateBindEnabled = true;
|
||||
}
|
||||
} catch (java.lang.ReflectiveOperationException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Field getFieldAt(final @NotNull Class<?> clazz,
|
||||
final @NotNull Class<?> type,
|
||||
final int index) {
|
||||
int currentIndex = 0;
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
field.setAccessible(true);
|
||||
|
||||
if (type.isAssignableFrom(field.getType())) {
|
||||
if (currentIndex++ == index) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return getFieldAt(clazz.getSuperclass(), type, index);
|
||||
}
|
||||
throw new IllegalStateException("Could not find field #" + index + " in " + clazz.getName());
|
||||
}
|
||||
|
||||
public static @NotNull Field findField(final boolean checkSuperClass,
|
||||
final @NotNull Class<?> clazz,
|
||||
final String @NotNull ... types) throws NoSuchFieldException {
|
||||
for (final Field field : clazz.getDeclaredFields()) {
|
||||
final String fieldTypeName = field.getType().getSimpleName();
|
||||
for (final String type : types) {
|
||||
if (fieldTypeName.equals(type)) {
|
||||
if (!Modifier.isPublic(field.getModifiers())) {
|
||||
field.setAccessible(true);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkSuperClass && clazz != Object.class && clazz.getSuperclass() != null) {
|
||||
return findField(true, clazz.getSuperclass(), types);
|
||||
}
|
||||
throw new NoSuchFieldException(types[0]);
|
||||
}
|
||||
|
||||
private @NotNull BukkitServerVersion resolveServerVersion() {
|
||||
final String bukkitVersion = Bukkit.getBukkitVersion();
|
||||
|
||||
for (final BukkitServerVersion serverVersion : BukkitServerVersion.REVERSED_VALUES) {
|
||||
if (bukkitVersion.contains(serverVersion.getRelease())) {
|
||||
return serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an error if we cannot find the server version
|
||||
throw new IllegalStateException("Could not find server version " + bukkitVersion);
|
||||
}
|
||||
|
||||
private boolean isObfuscated() {
|
||||
try {
|
||||
Class.forName("net.minecraft.server.network.PlayerConnection");
|
||||
return true;
|
||||
} catch (ClassNotFoundException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Class<?> getNMSClass(final String modern, final String legacy) throws ClassNotFoundException {
|
||||
return Class.forName(USES_LEGACY_PACKAGING ? LEGACY_NMS_PACKAGE + legacy : "net.minecraft." + modern);
|
||||
}
|
||||
|
||||
public @NotNull Class<?> getOBCClass(final String clazz) throws ClassNotFoundException {
|
||||
return Class.forName(OBC_PACKAGE + clazz);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void inject() {
|
||||
try {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
|
||||
|
||||
// Check if the field has the correct generic type;
|
||||
// We need this field to be List<ChannelFuture>
|
||||
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final var list = (List<?>) field.get(getFieldAt(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(SERVER_INSTANCE));
|
||||
|
||||
for (final Object object : list) {
|
||||
final ChannelFuture channelFuture = (ChannelFuture) object;
|
||||
final List<String> names = channelFuture.channel().pipeline().names();
|
||||
|
||||
ChannelHandler bootstrap = null;
|
||||
|
||||
for (final String name : names) {
|
||||
try {
|
||||
final ChannelHandler handler = channelFuture.channel().pipeline().get(name);
|
||||
final Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
|
||||
childHandlerField.setAccessible(true);
|
||||
final var childHandler = (ChannelInitializer<?>) childHandlerField.get(handler);
|
||||
|
||||
if (childHandler != null) {
|
||||
bootstrap = handler;
|
||||
}
|
||||
break;
|
||||
} catch (Exception exception) {
|
||||
// Some plugins modify this, so we better skip any exceptions
|
||||
}
|
||||
}
|
||||
|
||||
if (bootstrap == null) {
|
||||
// Default to the first channel future if we couldn't find a childHandler
|
||||
bootstrap = channelFuture.channel().pipeline().first();
|
||||
}
|
||||
|
||||
final Field childHandlerField = bootstrap.getClass().getDeclaredField("childHandler");
|
||||
childHandlerField.setAccessible(true);
|
||||
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
|
||||
|
||||
ChannelHandler finalBootstrap = bootstrap;
|
||||
|
||||
// Inject our own channel initializer into the original field
|
||||
// If the plugin is not fully enabled but executes the injection,
|
||||
// put an initializer to close the new connection automatically.
|
||||
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
|
||||
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
|
||||
@Override
|
||||
protected void initChannel(Channel channel) {
|
||||
channel.close();
|
||||
}
|
||||
};
|
||||
childHandlerField.set(finalBootstrap, initializer);
|
||||
}
|
||||
|
||||
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
|
||||
try {
|
||||
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
|
||||
pipeline -> {
|
||||
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
|
||||
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
|
||||
}
|
||||
));
|
||||
} catch (IllegalAccessException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.listener;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
|
||||
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
|
||||
public final class BukkitJoinListener implements Listener {
|
||||
private static MethodHandle _handleMethod;
|
||||
// ServerGamePacketListenerImpl aka. PlayerConnection
|
||||
private static MethodHandle _listenerField;
|
||||
private static MethodHandle _networkManagerField;
|
||||
private static MethodHandle _channelField;
|
||||
|
||||
private static boolean initialized;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Method handleMethod = FallbackBukkitInjector.getOBCClass("entity.CraftPlayer")
|
||||
.getDeclaredMethod("getHandle");
|
||||
final Field listenerField = FallbackBukkitInjector.findField(false, handleMethod.getReturnType(),
|
||||
"PlayerConnection", "ServerGamePacketListenerImpl");
|
||||
final Field networkManagerField = FallbackBukkitInjector.findField(true, listenerField.getType(),
|
||||
"NetworkManager", "Connection");
|
||||
final Field channelField = FallbackBukkitInjector.getFieldAt(networkManagerField.getType(), Channel.class, 0);
|
||||
|
||||
_handleMethod = MethodHandles.lookup().unreflect(handleMethod);
|
||||
_listenerField = MethodHandles.lookup().unreflectGetter(listenerField);
|
||||
_networkManagerField = MethodHandles.lookup().unreflectGetter(networkManagerField);
|
||||
_channelField = MethodHandles.lookup().unreflectGetter(channelField);
|
||||
initialized = true;
|
||||
} catch (Exception exception) {
|
||||
initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
// Make sure the player is not an NPC
|
||||
if (!initialized || event.getPlayer().hasMetadata("NPC")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Object handle = _handleMethod.invoke(event.getPlayer());
|
||||
final Object listener = _listenerField.invoke(handle);
|
||||
final Object networkManager = _networkManagerField.invoke(listener);
|
||||
final Channel channel = (Channel) _channelField.invoke(networkManager);
|
||||
|
||||
// Don't continue if the channel isn't found or if the channel is inactive
|
||||
if (channel == null || !channel.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player is not a fake player
|
||||
if (FakeChannelUtil.isFakePlayer(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the channel if Sonar's handlers are not found when the PlayerJoinEvent is called
|
||||
if (channel.pipeline().context(FALLBACK_BANDWIDTH) == null) {
|
||||
channel.close();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().warn("Couldn't find {}: {}", event.getPlayer().getName(), throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bungee") version "0.6.0"
|
||||
alias(libs.plugins.pluginyml.bungee) apply true
|
||||
}
|
||||
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
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")
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
compileOnly(rootProject.libs.bungeecord)
|
||||
testCompileOnly(rootProject.libs.bungeecord)
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
|
||||
implementation(rootProject.libs.adventure.platform.bungee) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bungee)
|
||||
implementation(rootProject.libs.libby.bungee)
|
||||
}
|
||||
|
||||
// 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")
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -19,28 +19,25 @@ package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import com.alessiodp.libby.BungeeLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackBungeeInjector;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +45,19 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
*/
|
||||
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
|
||||
|
||||
@Override
|
||||
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
|
||||
if (uniqueId == null) {
|
||||
return null;
|
||||
}
|
||||
return bungeeAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Audience sender(final @NotNull Object object) {
|
||||
return bungeeAudiences.sender((CommandSender) object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -72,34 +82,35 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
}
|
||||
};
|
||||
|
||||
private Metrics metrics;
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Add charts for some configuration options
|
||||
metrics.addCustomChart(new SimplePie("verification",
|
||||
() -> getConfig().getVerification().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("captcha",
|
||||
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
|
||||
metrics.addCustomChart(new SimplePie("language",
|
||||
() -> getConfig().getLanguage().getName()));
|
||||
metrics.addCustomChart(new SimplePie("database_type",
|
||||
() -> getConfig().getDatabase().getType().getDisplayName()));
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// 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();
|
||||
FallbackBungeeInjector.inject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,9 +18,11 @@
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
|
||||
@@ -34,7 +36,10 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
@Override
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
|
||||
Sonar.get().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
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.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
|
||||
|
||||
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER_GETTER = MethodHandles.privateLookupIn(HandlerBoss.class, MethodHandles.lookup())
|
||||
.findGetter(HandlerBoss.class, "channel", ChannelWrapper.class);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
final DefinedPacket wrappedPacket = packetWrapper.packet;
|
||||
// Don't handle any invalid packets
|
||||
if (wrappedPacket != null) {
|
||||
final Channel channel = ctx.channel();
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
final Handshake handshake = (Handshake) wrappedPacket;
|
||||
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
|
||||
// We don't care about server pings; remove the handler
|
||||
if (handshake.getRequestedProtocol() == 1) {
|
||||
ctx.channel().pipeline().remove(this);
|
||||
}
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
else if (wrappedPacket instanceof LoginRequest) {
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper;
|
||||
try {
|
||||
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
|
||||
} catch (final Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
// We've done our job - deject this pipeline
|
||||
ctx.channel().pipeline().remove(this);
|
||||
// Make sure to mark this packet as released
|
||||
packetWrapper.trySingleRelease();
|
||||
// Let Sonar process the login packet
|
||||
handleLogin(channel, ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
+12
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -22,12 +22,16 @@ import io.netty.channel.ChannelInitializer;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import sun.misc.Unsafe;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackInjectionHelper {
|
||||
public class FallbackBungeeInjector {
|
||||
public void inject() {
|
||||
try {
|
||||
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
|
||||
@@ -35,7 +39,9 @@ public class FallbackInjectionHelper {
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
|
||||
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
|
||||
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
|
||||
new FallbackBungeeInboundHandler()));
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
@@ -47,9 +53,9 @@ public class FallbackInjectionHelper {
|
||||
final long offset = unsafe.staticFieldOffset(childField);
|
||||
|
||||
// Replace the original channel initializer
|
||||
unsafe.putObject(base, offset, fallbackInitializer);
|
||||
unsafe.putObject(base, offset, injectedInitializer);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
-269
@@ -1,269 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.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
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.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));
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(rootProject.libs.imagefilters)
|
||||
|
||||
testCompileOnly(project(":api"))
|
||||
testCompileOnly(rootProject.libs.imagefilters)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.captcha;
|
||||
|
||||
import com.jhlabs.image.FBMFilter;
|
||||
import com.jhlabs.image.SmearFilter;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.captcha.filters.CurvesOverlayFilter;
|
||||
import xyz.jonesdev.sonar.captcha.filters.NoiseOverlayFilter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.font.FontRenderContext;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
|
||||
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.FONTS;
|
||||
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.STANDARD_FONT_SIZE;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class StandardCaptchaGenerator implements CaptchaGenerator {
|
||||
private static final CurvesOverlayFilter CURVES = new CurvesOverlayFilter(3);
|
||||
//private static final CircleInverseFilter CIRCLES = new CircleInverseFilter(1, 30, 10);
|
||||
private static final NoiseOverlayFilter NOISE = new NoiseOverlayFilter(1, 20);
|
||||
private static final SmearFilter SMEAR = new SmearFilter();
|
||||
private static final FBMFilter FBM = new FBMFilter();
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Color[] COLORS = new Color[4];
|
||||
private static final float[] COLOR_FRACTIONS = new float[COLORS.length];
|
||||
|
||||
static {
|
||||
FBM.setAmount(0.6f);
|
||||
FBM.setScale(15);
|
||||
|
||||
SMEAR.setShape(SmearFilter.SQUARES);
|
||||
SMEAR.setDensity(0.1f);
|
||||
SMEAR.setDistance(0);
|
||||
SMEAR.setMix(0.35f);
|
||||
|
||||
// Create fractions based on the number of colors
|
||||
for (int i = 0; i < COLOR_FRACTIONS.length; i++) {
|
||||
COLOR_FRACTIONS[i] = (float) i / (COLOR_FRACTIONS.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private final int width, height;
|
||||
private final @Nullable InputStream background;
|
||||
private BufferedImage backgroundImage;
|
||||
|
||||
@Override
|
||||
public @NotNull BufferedImage createImage(final char[] answer) {
|
||||
final BufferedImage image = createBackgroundImage();
|
||||
final Graphics2D graphics = image.createGraphics();
|
||||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
// Draw characters and other effects on the image
|
||||
applyRandomColorGradient(graphics);
|
||||
drawCharacters(graphics, answer);
|
||||
CURVES.transform(image, graphics);
|
||||
NOISE.transform(image);
|
||||
//SMEAR.filter(image, image);
|
||||
//CIRCLES.transform(image); // TODO: check if the text is still easy to read after this
|
||||
// Make sure to dispose the graphics instance
|
||||
graphics.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
private @NotNull BufferedImage createBackgroundImage() {
|
||||
if (background == null) {
|
||||
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
|
||||
// Fill the entire image with a noise texture
|
||||
return FBM.filter(image, image);
|
||||
}
|
||||
try {
|
||||
return backgroundImage == null ? backgroundImage = ImageIO.read(background) : backgroundImage;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Could not read background image", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyRandomColorGradient(final @NotNull Graphics2D graphics) {
|
||||
// Randomize the colors for the gradient effect
|
||||
for (int i = 0; i < COLORS.length; i++) {
|
||||
final float random = 0.9f + RANDOM.nextFloat() * 0.1f;
|
||||
COLORS[i] = Color.getHSBColor(RANDOM.nextFloat(), random, random);
|
||||
}
|
||||
|
||||
// Apply the random gradient effect
|
||||
graphics.setPaint(new LinearGradientPaint(0, 0, width, height,
|
||||
COLOR_FRACTIONS, COLORS, MultipleGradientPaint.CycleMethod.REFLECT));
|
||||
}
|
||||
|
||||
private void drawCharacters(final @NotNull Graphics2D graphics,
|
||||
final char @NotNull [] answer) {
|
||||
final FontRenderContext ctx = graphics.getFontRenderContext();
|
||||
final GlyphVector[] glyphs = new GlyphVector[answer.length];
|
||||
|
||||
for (int i = 0; i < answer.length; i++) {
|
||||
// Create a glyph vector for the character with a random font
|
||||
final Font font = FONTS[RANDOM.nextInt(FONTS.length)];
|
||||
glyphs[i] = font.createGlyphVector(ctx, String.valueOf(answer[i]));
|
||||
}
|
||||
|
||||
final double scalingXY = 5 - Math.min(answer.length, 5) * 0.65;
|
||||
|
||||
// Calculate first X and Y positions
|
||||
final double totalWidth = Arrays.stream(glyphs)
|
||||
.mapToDouble(glyph -> glyph.getLogicalBounds().getWidth() * scalingXY - 1)
|
||||
.sum();
|
||||
double beginX = Math.max(Math.min(width / 2D - totalWidth / 2D, totalWidth), 0);
|
||||
double beginY = (height + STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
|
||||
|
||||
// Draw each glyph one by one
|
||||
for (final GlyphVector glyph : glyphs) {
|
||||
final AffineTransform transformation = AffineTransform.getTranslateInstance(beginX, beginY);
|
||||
// Shear the glyph by a random amount
|
||||
final double shearXY = Math.sin(beginX + beginY) / 6;
|
||||
transformation.shear(shearXY, shearXY);
|
||||
// Scale the glyph to perfectly fit the image
|
||||
transformation.scale(scalingXY, scalingXY);
|
||||
// Draw the glyph to the buffered image
|
||||
final Shape transformedShape = transformation.createTransformedShape(glyph.getOutline());
|
||||
graphics.fill(transformedShape);
|
||||
// Draw a random outline around the glyph
|
||||
if (RANDOM.nextFloat() < 0.25f) {
|
||||
createGlyphOutline(graphics, transformedShape);
|
||||
}
|
||||
// Make sure the next glyph isn't drawn at the same position
|
||||
beginX += glyph.getVisualBounds().getWidth() * scalingXY + 2;
|
||||
beginY += -10 + RANDOM.nextFloat() * 20;
|
||||
}
|
||||
}
|
||||
|
||||
private void createGlyphOutline(final @NotNull Graphics2D graphics, final @NotNull Shape shape) {
|
||||
final float txy = 1.25f + RANDOM.nextFloat();
|
||||
final float width = 1 + RANDOM.nextFloat();
|
||||
|
||||
// Create a randomly translated and stroked shape based on the original glyph shape
|
||||
final AffineTransform translation = AffineTransform.getTranslateInstance(txy, txy);
|
||||
final Shape strokedShape = new BasicStroke(width).createStrokedShape(shape);
|
||||
|
||||
graphics.fill(translation.createTransformedShape(strokedShape));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.captcha;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
class StandardTTFFontProvider {
|
||||
private static final String[] FONT_NAMES = {"Kingthings_Trypewriter_2"};
|
||||
static final Font[] FONTS = new Font[FONT_NAMES.length];
|
||||
static final int STANDARD_FONT_SIZE = 25;
|
||||
|
||||
static {
|
||||
for (int i = 0; i < FONT_NAMES.length; i++) {
|
||||
FONTS[i] = loadFont(String.format("/assets/fonts/%s.ttf", FONT_NAMES[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private static Font loadFont(final @NotNull String path) {
|
||||
try (final InputStream inputStream = StandardCaptchaGenerator.class.getResourceAsStream(path)) {
|
||||
// Load the font from the TTF file
|
||||
final Font customFont = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(inputStream));
|
||||
// Set the font size and style
|
||||
return customFont.deriveFont(Font.PLAIN, STANDARD_FONT_SIZE);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class CircleInverseFilter {
|
||||
private final int amount, minRadius, maxRadiusExpansion;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image) {
|
||||
final int minX = image.getWidth() / 4;
|
||||
final int minY = image.getHeight() / 4;
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
// Generate random position and radius for the circle
|
||||
final float centerX = minX + RANDOM.nextInt(image.getWidth() - minX * 2);
|
||||
final float centerY = minY + RANDOM.nextInt(image.getHeight() - minY * 2);
|
||||
final int radius = minRadius + RANDOM.nextInt(maxRadiusExpansion);
|
||||
|
||||
final Circle circle = new Circle(centerX, centerY, radius);
|
||||
// Draw inverted circle
|
||||
_addInvertedCircle(image, circle);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _addInvertedCircle(final @NotNull BufferedImage bufferedImage,
|
||||
final @NotNull Circle circle) {
|
||||
for (int x = 0; x < bufferedImage.getWidth(); x++) {
|
||||
for (int y = 0; y < bufferedImage.getHeight(); y++) {
|
||||
if (isWithinCircle(x, y, circle, circle.radius)) {
|
||||
// Invert colors within the circle
|
||||
final int rgb = bufferedImage.getRGB(x, y);
|
||||
final int invertedRGB = invertColorAndFilter(rgb);
|
||||
bufferedImage.setRGB(x, y, invertedRGB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isWithinCircle(final float x, final float y, final @NotNull Circle circle, int radius) {
|
||||
final float distanceX = x - circle.centerX;
|
||||
final float distanceY = y - circle.centerY;
|
||||
return distanceX * distanceX + distanceY * distanceY <= radius * radius;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
static final class Circle {
|
||||
private final float centerX;
|
||||
private final float centerY;
|
||||
private final int radius;
|
||||
}
|
||||
|
||||
private static int invertColorAndFilter(final int rgb) {
|
||||
final Color color = new Color(rgb);
|
||||
final int red = Math.max(Color.LIGHT_GRAY.getRed() - color.getRed(), 0);
|
||||
final int green = Math.max(Color.LIGHT_GRAY.getGreen() - color.getGreen(), 0);
|
||||
final int blue = Math.max(Color.LIGHT_GRAY.getBlue() - color.getBlue(), 0);
|
||||
return new Color(red, green, blue).getRGB();
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class CurvesOverlayFilter {
|
||||
private final int amount;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image,
|
||||
final @NotNull Graphics2D graphics) {
|
||||
// Randomize the stroke width
|
||||
graphics.setStroke(new BasicStroke(1 + RANDOM.nextFloat()));
|
||||
|
||||
final int halfWidth = image.getWidth() / 2;
|
||||
|
||||
for (int i = 0; i < amount; ++i) {
|
||||
final float randomX = image.getWidth() * RANDOM.nextFloat();
|
||||
final float randomY = image.getHeight() * RANDOM.nextFloat();
|
||||
final float amplitude = 6.2831855f * (RANDOM.nextFloat() - 0.5f);
|
||||
final float sin = (float) Math.sin(amplitude) * halfWidth;
|
||||
final float cos = (float) Math.cos(amplitude) * halfWidth;
|
||||
final float x1 = randomX - cos;
|
||||
final float y1 = randomY - sin;
|
||||
final float x2 = randomX + cos;
|
||||
final float y2 = randomY + sin;
|
||||
// Control points for the cubic curve
|
||||
final float ctrlX1 = randomX + sin / 2;
|
||||
final float ctrlY1 = randomY - cos / 2;
|
||||
final float ctrlX2 = randomX - sin / 2;
|
||||
final float ctrlY2 = randomY + cos / 2;
|
||||
// Draw a cubic curve instead of a straight line
|
||||
graphics.draw(new CubicCurve2D.Float(x1, y1, ctrlX1, ctrlY1, ctrlX2, ctrlY2, x2, y2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class GridOverlayFilter {
|
||||
private final int amount, lineWidth, randomOffset;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public void transform(final @NotNull BufferedImage image,
|
||||
final @NotNull Graphics2D graphics) {
|
||||
final int spacingX = image.getWidth() / amount + lineWidth;
|
||||
final int spacingY = image.getHeight() / amount + lineWidth;
|
||||
int currentX = spacingX / 2;
|
||||
int currentY = spacingY / 2;
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
graphics.setStroke(new BasicStroke(lineWidth * 0.5f + lineWidth * RANDOM.nextFloat()));
|
||||
graphics.drawLine(currentX, 0, currentX + RANDOM.nextInt(randomOffset), image.getHeight());
|
||||
graphics.drawLine(0, currentY, image.getWidth(), currentY + RANDOM.nextInt(randomOffset));
|
||||
|
||||
currentX += spacingX + lineWidth;
|
||||
currentY += spacingY + lineWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.captcha.filters;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class NoiseOverlayFilter {
|
||||
private final float density, amount;
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private int randomGaussian(int x) {
|
||||
x += (int) (RANDOM.nextGaussian() * amount);
|
||||
return Math.max(Math.min(x, 0xff), 0);
|
||||
}
|
||||
|
||||
public void transform(final @NotNull BufferedImage image) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
if (RANDOM.nextFloat() <= density) {
|
||||
final int rgb = image.getRGB(x, y);
|
||||
int r = (rgb >> 16) & 0xff;
|
||||
int g = (rgb >> 8) & 0xff;
|
||||
int b = rgb & 0xff;
|
||||
r = randomGaussian(r);
|
||||
g = randomGaussian(g);
|
||||
b = randomGaussian(b);
|
||||
image.setRGB(x, y, (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
@UtilityClass
|
||||
public class Main {
|
||||
public void main(final String... args) throws IOException {
|
||||
final Random random = new Random();
|
||||
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(128, 128, null);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
final char[] dictionary = {'a', 'b', 'c', 'd', 'e', 'f'/*, 'g'*/, 'h'/*, 'i'*/, 'j',
|
||||
'k', 'm', 'n', 'o', 'p'/*, 'q'*/, 'r', 's', 't', 'u'/*, 'v', 'w'*/, 'x', 'y', 'z'};
|
||||
|
||||
// Generate image
|
||||
final int amount = 1;
|
||||
for (int i = 0; i < amount; i++) {
|
||||
final char[] answer = new char[3 + random.nextInt(2)];
|
||||
for (int j = 0; j < answer.length; j++) {
|
||||
answer[j] = dictionary[random.nextInt(dictionary.length)];
|
||||
}
|
||||
final BufferedImage bufferedImage = standardCaptchaGenerator.createImage(answer);
|
||||
|
||||
// Save image
|
||||
ImageIO.write(bufferedImage, "png", new File(i + ".png"));
|
||||
}
|
||||
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to generate " + amount + " image(s)");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
implementation(project(":captcha"))
|
||||
compileOnly(rootProject.libs.adventure.nbt)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -21,10 +21,11 @@ import com.alessiodp.libby.Library;
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
|
||||
@UtilityClass
|
||||
class LibraryLoader {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager) {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull SonarPlatform platform) {
|
||||
libraryManager.addMavenCentral();
|
||||
libraryManager.addJitPack();
|
||||
libraryManager.loadLibraries(
|
||||
@@ -39,12 +40,12 @@ class LibraryLoader {
|
||||
Library.builder()
|
||||
.groupId("com{}google{}code{}gson")
|
||||
.artifactId("gson")
|
||||
.version("2.10.1")
|
||||
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
|
||||
.version("2.11.0")
|
||||
.relocate("com{}google{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
|
||||
.build(),
|
||||
// Caffeine
|
||||
Library.builder()
|
||||
.groupId("com.github.ben-manes.caffeine")
|
||||
.groupId("com{}github{}ben-manes{}caffeine")
|
||||
.artifactId("caffeine")
|
||||
.version("3.1.8")
|
||||
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
|
||||
@@ -56,12 +57,37 @@ class LibraryLoader {
|
||||
.version("6.1")
|
||||
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
|
||||
.build(),
|
||||
// MySQL driver
|
||||
// JHLabs image filters
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("8.3.0")
|
||||
.groupId("com{}jhlabs")
|
||||
.artifactId("filters")
|
||||
.version("2.0.235-1")
|
||||
.relocate("com{}jhlabs", "xyz{}jonesdev{}sonar{}libs{}jhlabs")
|
||||
.build()
|
||||
);
|
||||
|
||||
// Only load adventure if not on Velocity
|
||||
if (platform != SonarPlatform.VELOCITY) {
|
||||
libraryManager.loadLibraries(
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-minimessage")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build(),
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-serializer-gson")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build(),
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-nbt")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -19,6 +19,9 @@ package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Ticker;
|
||||
import com.j256.ormlite.logger.Level;
|
||||
import com.j256.ormlite.logger.Logger;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
@@ -29,12 +32,17 @@ 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.statistics.SonarStatistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommands.*;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
@@ -49,35 +57,48 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private Notification notificationHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final LibraryManager libraryManager;
|
||||
private final SonarStatistics statistics;
|
||||
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) {
|
||||
final @NotNull SonarPlatform platform,
|
||||
final @NotNull File dataDirectory,
|
||||
final @NotNull LibraryManager libraryManager) {
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager, platform);
|
||||
this.libraryManager = libraryManager;
|
||||
// Check if Netty is up to date
|
||||
ProtocolUtil.checkNettyVersion();
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
// Store our library manager for the API
|
||||
this.libraryManager = libraryManager;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Set the plugin instance
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.platform = platform;
|
||||
// Load the rest of the components
|
||||
this.statistics = new GlobalSonarStatistics();
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
// Register all subcommands
|
||||
this.subcommandRegistry = new SubcommandRegistry();
|
||||
this.subcommandRegistry.register(
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand(),
|
||||
new NotifyCommand());
|
||||
// Hide unnecessary debug information
|
||||
Logger.setGlobalLogLevel(Level.WARNING);
|
||||
}
|
||||
|
||||
public final void initialize() {
|
||||
// Check if the branch is not the main branch to warn about unstable versions
|
||||
if (!getVersion().isOnMainBranch()) {
|
||||
if (!getVersion().getGitBranch().equals("main")) {
|
||||
getLogger().warn("You are currently using an unofficial experimental branch.");
|
||||
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
|
||||
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
@@ -96,6 +117,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
|
||||
// Start threads
|
||||
getLogger().info("Starting all managed threads...");
|
||||
SonarServiceManager.start();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
} catch (Throwable throwable) {
|
||||
@@ -105,8 +130,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
return; // Do not check for updates if the launch failed
|
||||
}
|
||||
|
||||
// Check if a new version has been released
|
||||
UpdateChecker.checkForUpdates();
|
||||
// Check if a new version has been released if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
|
||||
UpdateChecker.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void enable();
|
||||
@@ -118,16 +145,32 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Warn player if they reloaded and changed 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.");
|
||||
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
|
||||
}
|
||||
|
||||
// Update the CAPTCHA generator if necessary
|
||||
if (getConfig().getVerification().getMap().getTiming() == SonarConfiguration.Verification.Timing.NEVER) {
|
||||
getFallback().setCaptchaGenerator(null);
|
||||
} else if (getFallback().getCaptchaGenerator() == null
|
||||
|| getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
|
||||
getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
|
||||
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
|
||||
} else {
|
||||
getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
|
||||
}
|
||||
|
||||
// Prepare cached packets
|
||||
getLogger().info("Taking cached snapshots of all packets...");
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter cache
|
||||
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
|
||||
// Update ratelimiter caches
|
||||
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
@@ -136,9 +179,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// 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
|
||||
// Create a new cache with the configured blacklist time
|
||||
getFallback().setBlacklist(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
// Store the new blacklist time, so we don't have to reset the blacklist every reload
|
||||
getFallback().setBlacklistTime(blacklistTime);
|
||||
@@ -149,14 +193,27 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
}
|
||||
|
||||
// Reinitialize database controller
|
||||
verifiedPlayerController = new VerifiedPlayerController();
|
||||
if (verifiedPlayerController != null) {
|
||||
// Close the old connection first
|
||||
verifiedPlayerController.close();
|
||||
}
|
||||
verifiedPlayerController = new VerifiedPlayerController(libraryManager);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
public final void shutdown() {
|
||||
// Initialize the shutdown process
|
||||
getLogger().info("Starting shutdown process...");
|
||||
|
||||
// ...
|
||||
|
||||
// Interrupt threads
|
||||
SonarServiceManager.stop();
|
||||
// Close database connection if present
|
||||
if (verifiedPlayerController != null) {
|
||||
verifiedPlayerController.close();
|
||||
}
|
||||
// Run the per-platform disable method
|
||||
disable();
|
||||
// Thank the user for using Sonar
|
||||
getLogger().info("Successfully shut down. Goodbye!");
|
||||
}
|
||||
|
||||
public abstract void disable();
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -26,8 +26,8 @@ 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;
|
||||
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
|
||||
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -39,7 +39,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
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
|
||||
// Increment the incoming traffic by the number of readable bytes
|
||||
INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
@@ -52,7 +52,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
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
|
||||
// Increment the outgoing traffic by the number of readable bytes
|
||||
OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Consumer<ChannelPipeline> sonarPipelineInjector;
|
||||
@Setter
|
||||
@Getter
|
||||
private InetAddress inetAddress;
|
||||
|
||||
@Override
|
||||
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
GlobalSonarStatistics.countConnection();
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
// Add the packet handler pipeline
|
||||
sonarPipelineInjector.accept(ctx.pipeline());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// The player can disconnect without sending the login packet first
|
||||
// Account for this by checking if the inetAddress has been set yet
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the connected players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
|
||||
// Remove the IP address from the queue
|
||||
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
|
||||
// Remove this account from the online players or decrement the number of accounts with the same IP
|
||||
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
// We can override the default exceptionCaught method since this handler
|
||||
// will run before the connection knows that there has been an error.
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Throwable cause) throws Exception {
|
||||
// Close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.GeyserUtil;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
|
||||
protected @Nullable String username;
|
||||
protected ProtocolVersion protocolVersion;
|
||||
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
|
||||
|
||||
protected static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
/**
|
||||
* Validates and handles incoming handshake packets
|
||||
*
|
||||
* @param hostname Hostname (server address) sent by the client
|
||||
* @param protocol Protocol version number sent by the client
|
||||
*/
|
||||
protected final void handleHandshake(final @NotNull Channel channel,
|
||||
final @NotNull String hostname,
|
||||
final int protocol) throws Exception {
|
||||
// Check if the hostname is invalid
|
||||
if (hostname.isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming login packets
|
||||
*
|
||||
* @param channel Forwarded client channel
|
||||
* @param loginPacket Login packet sent by the client
|
||||
* @param username Username sent by the client
|
||||
* @param socketAddress Socket address of the client
|
||||
*/
|
||||
protected final void handleLogin(final @NotNull Channel channel,
|
||||
final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Runnable loginPacket,
|
||||
final @NotNull String username,
|
||||
final @NotNull InetSocketAddress socketAddress) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (this.username != null) {
|
||||
throw new CorruptedFrameException("Already sent login");
|
||||
}
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections
|
||||
if (protocolVersion.isUnknown()) {
|
||||
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
|
||||
throw new CorruptedFrameException("Unknown protocol version");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username and IP address
|
||||
this.username = username;
|
||||
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
|
||||
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player with the same IP address
|
||||
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections if configured
|
||||
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
|
||||
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUuidString = "OfflinePlayer:" + username;
|
||||
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inboundHandler.getInetAddress().toString(), offlineUuid)) {
|
||||
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all other pipelines that could still mess up something
|
||||
rewriteProtocol(ctx, channelRemovalListener);
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (runnable != null) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued);
|
||||
// Remove other instances of this IP address from the queue
|
||||
return null;
|
||||
}
|
||||
|
||||
return () -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(username).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(
|
||||
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(username, offlineUuid);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all pipelines and rewrites them using our custom handlers
|
||||
*/
|
||||
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull RemovalListener removalListener) {
|
||||
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
|
||||
// Don't accidentally remove Sonar's handlers
|
||||
if (entry.getKey().startsWith("sonar")
|
||||
// Don't remove floodgate's pipelines
|
||||
|| entry.getKey().startsWith("geyser")
|
||||
|| entry.getKey().startsWith("floodgate")
|
||||
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
|
||||
continue;
|
||||
}
|
||||
ctx.pipeline().remove(entry.getValue());
|
||||
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
|
||||
}
|
||||
// Add our read/write timeout handler
|
||||
ctx.pipeline().addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
|
||||
Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
Sonar.get().getConfig().getVerification().getWriteTimeout(),
|
||||
TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the maximum accounts per IP limit check before letting the player join
|
||||
*
|
||||
* @param ctx Forwarded client channel context
|
||||
* @param inetAddress Resolved client IP address
|
||||
* @param loginPacket Login packet sent by the client
|
||||
*/
|
||||
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull Runnable loginPacket) throws Exception {
|
||||
// Increment the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
|
||||
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Skip the maximum online per IP check if it's disabled in the configuration
|
||||
if (maxOnlinePerIp > 0) {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Let the server know about the login packet
|
||||
loginPacket.run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect the player before verification (during login)
|
||||
* by replacing the encoder before running the method.
|
||||
*
|
||||
* @param packet Disconnect packet
|
||||
* @param channel Channel of the player
|
||||
* @param protocolVersion Protocol version of the player
|
||||
*/
|
||||
private void customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet) {
|
||||
if (channel.eventLoop().inEventLoop()) {
|
||||
_customDisconnect(channel, protocolVersion, packet);
|
||||
} else {
|
||||
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
|
||||
}
|
||||
}
|
||||
|
||||
private void _customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet) {
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
|
||||
if (channel.pipeline().context(handler) != null) {
|
||||
channel.pipeline().remove(handler);
|
||||
}
|
||||
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
|
||||
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
|
||||
// Close the channel if no decoder exists
|
||||
if (currentEncoder != null) {
|
||||
// We don't need to update the encoder if it's already present
|
||||
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
}
|
||||
closeWith(channel, protocolVersion, packet);
|
||||
} else {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RemovalListener {
|
||||
void accept(final @NotNull ChannelPipeline pipeline,
|
||||
final @NotNull String name,
|
||||
final @NotNull ChannelHandler handler);
|
||||
|
||||
RemovalListener EMPTY = (pipeline, name, handler) -> {};
|
||||
}
|
||||
}
|
||||
+9
-22
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
* 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
|
||||
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,30 +25,18 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
private boolean knownDisconnect;
|
||||
|
||||
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
|
||||
super(timeout, 0L, 0L, timeUnit);
|
||||
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
|
||||
super(readTimeout, writeTimeout, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
protected void channelIdle(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
assert idleStateEvent.state() == IdleState.READER_IDLE;
|
||||
readTimedOut(ctx);
|
||||
}
|
||||
|
||||
private void readTimedOut(final ChannelHandlerContext ctx) {
|
||||
if (!knownDisconnect) {
|
||||
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
if (ctx.channel().isActive()) {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
knownDisconnect = true;
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
if (ctx.channel().isActive()) {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 239 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user