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 | ||
|
|
23a472d3aa | ||
|
|
76f0f4abda | ||
|
|
720e1cb1cb | ||
|
|
a98d9337c7 | ||
|
|
5158111edf | ||
|
|
2ab829b43e | ||
|
|
04bbfde6b6 | ||
|
|
ef8abcec46 | ||
|
|
1fb38ec8d4 | ||
|
|
cc5e958e2a | ||
|
|
1437204a15 | ||
|
|
c96d733920 | ||
|
|
c561b3eeaf | ||
|
|
2310864816 | ||
|
|
55030c5073 | ||
|
|
18bb9557b1 | ||
|
|
5d02050815 | ||
|
|
5914b23ae5 | ||
|
|
7ce56a79ab | ||
|
|
7b17ba7e53 | ||
|
|
8268cda660 | ||
|
|
89d0c7346f | ||
|
|
512faa956e | ||
|
|
1f6781aaa3 | ||
|
|
bf40333de5 | ||
|
|
31b961b577 | ||
|
|
e4821781fa | ||
|
|
bdcb7d28a7 | ||
|
|
433fab10f1 | ||
|
|
014b956f9e | ||
|
|
4be1a970d0 | ||
|
|
bea1d292ee | ||
|
|
468a11e0be | ||
|
|
cf2fce9c29 | ||
|
|
cc52289500 | ||
|
|
ac1f6d616b | ||
|
|
bdd941171e | ||
|
|
1e18a29d1e | ||
|
|
5671c2029e | ||
|
|
32aeee4f06 | ||
|
|
7b5ce41420 | ||
|
|
a66f60f177 |
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.
|
||||
+19
-50
@@ -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)
|
||||
|
|
||||
@@ -25,70 +23,41 @@
|
||||
|
|
||||
[Discord](https://jonesdev.xyz/discord)
|
||||
|
|
||||
[License](https://github.com/jonesdevelopment/sonar/blob/main/README.md#license)
|
||||
[License](https://github.com/jonesdevelopment/sonar/?tab=readme-ov-file#license)
|
||||
</div>
|
||||
|
||||
## Design and Goal
|
||||
|
||||
* Effective, lightweight, and easy-to-use
|
||||
* No unnecessary features and clean code
|
||||
* Protection against all kinds of bot attacks
|
||||
* No player should be annoyed by any sort of captcha
|
||||
* No player should be annoyed by any sort of [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)
|
||||
* No sort of checking for VPNs or proxies
|
||||
* Multi-platform support (Velocity, BungeeCord, and Bukkit)
|
||||
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
|
||||
|
||||
## Checks
|
||||
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.
|
||||
|
||||
- New player logins are queued to prevent spam login attacks.
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
|
||||
all types of bots.
|
||||
- Spigot & BungeeCord
|
||||
- Duplicate login/status packet spam fix
|
||||
- Run server ping handling asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
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 from huge spambot attacks since it queues the incoming connections, therefore making it
|
||||
technically impossible to have a ton of bots join the server at the same time.
|
||||
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. For example, some higher Minecraft versions have a bug where the client sometimes sends a packet out of order. Sonar accounts for that and does not falsely blacklist clients.
|
||||
If you or one of your players experiences a false positive, make sure to report it by opening
|
||||
a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket
|
||||
on [Discord](https://jonesdev.xyz/discord/).
|
||||
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.
|
||||
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).
|
||||
+3
-7
@@ -13,13 +13,9 @@ If you discover a security vulnerability within Sonar, please follow these steps
|
||||
|
||||
## Contact
|
||||
|
||||
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at admin@jonesdev.xyz.
|
||||
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at contact@jonesdev.xyz.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to the following people for their contributions:
|
||||
- [xism4](https://github.com/xism4)
|
||||
- fixed serious issues with packet decode logic on BungeeCord [#5](https://github.com/jonesdevelopment/sonar-legacy/pull/5) (Severity: medium)
|
||||
- fixed a false blacklisting issue when a player looses connection [#9](https://github.com/jonesdevelopment/sonar-legacy/pull/9) (Severity: low)
|
||||
- [NoGodHenry](https://github.com/NoGodHenry)
|
||||
- fixed a ConcurrentModificationException in the configuration [#3](https://github.com/jonesdevelopment/sonar-legacy/pull/3) (Severity: low)
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors),
|
||||
and also those who reported security issues or bugs.
|
||||
@@ -30,6 +30,5 @@ gradlew*
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.log
|
||||
*.yml
|
||||
*.svg
|
||||
*.txt
|
||||
+69
-83
@@ -1,107 +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
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
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")
|
||||
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.104.Final") // netty
|
||||
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("Sonar.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
exclude("net.kyori.adventure.audience.**")
|
||||
}
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/versions/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
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.13
|
||||
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
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
rootProject.name = "sonar"
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
dependencies {
|
||||
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
repositories {
|
||||
maven(url = "https://jitpack.io/") // simple-yaml
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
dependencies {
|
||||
compileOnly(rootProject.libs.simpleyaml) {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
compileOnly(rootProject.libs.annotations)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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 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;
|
||||
|
||||
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;
|
||||
@@ -27,18 +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.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
|
||||
*/
|
||||
@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
|
||||
@@ -50,8 +59,6 @@ public interface Sonar {
|
||||
*/
|
||||
@NotNull LoggerWrapper getLogger();
|
||||
|
||||
@NotNull File getDataDirectory();
|
||||
|
||||
@NotNull SonarConfiguration getConfig();
|
||||
|
||||
@NotNull SubcommandRegistry getSubcommandRegistry();
|
||||
@@ -60,12 +67,24 @@ public interface Sonar {
|
||||
|
||||
@NotNull Verbose getVerboseHandler();
|
||||
|
||||
@NotNull Notification getNotificationHandler();
|
||||
|
||||
@NotNull SystemTimer getLaunchTimer();
|
||||
|
||||
@NotNull SonarStatistics getStatistics();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setVerboseHandler(final @NotNull Verbose verboseHandler);
|
||||
|
||||
/**
|
||||
* Set a custom notification handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setNotificationHandler(final @NotNull Notification notificationHandler);
|
||||
|
||||
/**
|
||||
* Reloads the entire plugin
|
||||
*/
|
||||
@@ -73,7 +92,7 @@ public interface Sonar {
|
||||
|
||||
@NotNull
|
||||
default SonarVersion getVersion() {
|
||||
return SonarVersion.GET;
|
||||
return SonarVersion.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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 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 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,15 +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.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;
|
||||
@@ -42,7 +42,8 @@ public final class AttackTracker implements JVMProfiler {
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private long peakJoinsPerSecond;
|
||||
private long peakConnectionsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
@@ -50,9 +51,9 @@ public final class AttackTracker implements JVMProfiler {
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().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.
|
||||
@@ -65,8 +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().observe();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -77,6 +79,11 @@ public final class AttackTracker implements JVMProfiler {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
|
||||
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
|
||||
// Update connections per second peak if necessary
|
||||
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
|
||||
}
|
||||
final double processCPUUsage = getProcessCPUUsage();
|
||||
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
|
||||
// Update cpu usage peak if necessary
|
||||
@@ -94,39 +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 long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
|
||||
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
|
||||
.ifPresent(webhook -> {
|
||||
// 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
|
||||
@@ -139,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 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 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 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,16 +18,16 @@
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
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.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -36,91 +36,65 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
|
||||
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;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
|
||||
|
||||
// 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 long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(source);
|
||||
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
|
||||
@@ -129,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,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 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 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 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 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
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public enum Language {
|
||||
SYSTEM("system"),
|
||||
AB("Abkhazian"),
|
||||
AA("Afar"),
|
||||
AF("Afrikaans"),
|
||||
SQ("Albanian"),
|
||||
AM("Amharic"),
|
||||
AR("Arabic"),
|
||||
AN("Aragonese"),
|
||||
HY("Armenian"),
|
||||
AS("Assamese"),
|
||||
AE("Avestan"),
|
||||
AY("Aymara"),
|
||||
AZ("Azerbaijani"),
|
||||
BA("Bashkir"),
|
||||
EU("Basque"),
|
||||
BE("Belarusian"),
|
||||
BN("Bengali"),
|
||||
BH("Bihari"),
|
||||
BI("Bislama"),
|
||||
BS("Bosnian"),
|
||||
BR("Breton"),
|
||||
BG("Bulgarian"),
|
||||
MY("Burmese"),
|
||||
CA("Catalan"),
|
||||
CH("Chamorro"),
|
||||
CE("Chechen"),
|
||||
ZH("Chinese"),
|
||||
CU("Slavic"),
|
||||
CV("Chuvash"),
|
||||
KW("Cornish"),
|
||||
CO("Corsican"),
|
||||
HR("Croatian"),
|
||||
CS("Czech"),
|
||||
DA("Danish"),
|
||||
DV("Maldivian"),
|
||||
NL("Dutch"),
|
||||
DZ("Dzongkha"),
|
||||
EN("English"),
|
||||
EO("Esperanto"),
|
||||
ET("Estonian"),
|
||||
FO("Faroese"),
|
||||
FJ("Fijian"),
|
||||
FI("Finnish"),
|
||||
FR("French"),
|
||||
GD("Gaelic"),
|
||||
GL("Galician"),
|
||||
KA("Georgian"),
|
||||
DE("German"),
|
||||
EL("Greek"),
|
||||
GN("Guarani"),
|
||||
GU("Gujarati"),
|
||||
HT("Haitian Creole"),
|
||||
HA("Hausa"),
|
||||
HE("Hebrew"),
|
||||
HZ("Herero"),
|
||||
HI("Hindi"),
|
||||
HO("Hiri Motu"),
|
||||
HU("Hungarian"),
|
||||
IS("Icelandic"),
|
||||
IO("Ido"),
|
||||
ID("Indonesian"),
|
||||
IE("Interlingue"),
|
||||
IU("Inuktitut"),
|
||||
IK("Inupiaq"),
|
||||
GA("Irish"),
|
||||
IT("Italian"),
|
||||
JA("Japanese"),
|
||||
JV("Javanese"),
|
||||
KL("Kalaallisut"),
|
||||
KN("Kannada"),
|
||||
KS("Kashmiri"),
|
||||
KK("Kazakh"),
|
||||
KM("Khmer"),
|
||||
KI("Kikuyu"),
|
||||
RW("Kinyarwanda"),
|
||||
KY("Kirghiz"),
|
||||
KV("Komi"),
|
||||
KO("Korean"),
|
||||
KJ("Kwanyama"),
|
||||
KU("Kurdish"),
|
||||
LO("Lao"),
|
||||
LA("Latin"),
|
||||
LV("Latvian"),
|
||||
LI("Limburgish"),
|
||||
LN("Lingala"),
|
||||
LT("Lithuanian"),
|
||||
LB("Luxembourgish"),
|
||||
MK("Macedonian"),
|
||||
MG("Malagasy"),
|
||||
MS("Malay"),
|
||||
ML("Malayalam"),
|
||||
MT("Maltese"),
|
||||
GV("Manx"),
|
||||
MI("Maori"),
|
||||
MR("Marathi"),
|
||||
MH("Marshallese"),
|
||||
MO("Moldavian"),
|
||||
MN("Mongolian"),
|
||||
NA("Nauru"),
|
||||
NV("Navajo"),
|
||||
ND("Ndebele, North"),
|
||||
NR("Ndebele, South"),
|
||||
NG("Ndonga"),
|
||||
NE("Nepali"),
|
||||
SE("Northern Sami"),
|
||||
NO("Norwegian"),
|
||||
NB("Norwegian Bokmal"),
|
||||
NN("Norwegian Nynorsk"),
|
||||
NY("Chewa"),
|
||||
OC("Occitan"),
|
||||
OR("Oriya"),
|
||||
OM("Oromo"),
|
||||
OS("Ossetian"),
|
||||
PI("Pali"),
|
||||
PA("Panjabi"),
|
||||
FA("Persian"),
|
||||
PL("Polish"),
|
||||
PT("Portuguese"),
|
||||
PS("Pushto"),
|
||||
QU("Quechua"),
|
||||
RM("Raeto-Romance"),
|
||||
RO("Romanian"),
|
||||
RN("Rundi"),
|
||||
RU("Russian"),
|
||||
SM("Samoan"),
|
||||
SG("Sango"),
|
||||
SA("Sanskrit"),
|
||||
SC("Sardinian"),
|
||||
SR("Serbian"),
|
||||
SN("Shona"),
|
||||
II("Sichuan Yi"),
|
||||
SD("Sindhi"),
|
||||
SI("Sinhala"),
|
||||
SK("Slovak"),
|
||||
SL("Slovenian"),
|
||||
SO("Somali"),
|
||||
ST("Sotho, Southern"),
|
||||
ES("Spanish"),
|
||||
SU("Sundanese"),
|
||||
SW("Swahili"),
|
||||
SS("Swati"),
|
||||
SV("Swedish"),
|
||||
TL("Tagalog"),
|
||||
TY("Tahitian"),
|
||||
TG("Tajik"),
|
||||
TA("Tamil"),
|
||||
TT("Tatar"),
|
||||
TE("Telugu"),
|
||||
TH("Thai"),
|
||||
BO("Tibetan"),
|
||||
TI("Tigrinya"),
|
||||
TO("Tonga"),
|
||||
TS("Tsonga"),
|
||||
TN("Tswana"),
|
||||
TR("Turkish"),
|
||||
TK("Turkmen"),
|
||||
TW("Twi"),
|
||||
UG("Uighur"),
|
||||
UK("Ukrainian"),
|
||||
UR("Urdu"),
|
||||
UZ("Uzbek"),
|
||||
VI("Vietnamese"),
|
||||
VO("Volapuk"),
|
||||
WA("Walloon"),
|
||||
CY("Welsh"),
|
||||
FY("Western Frisian"),
|
||||
WO("Wolof"),
|
||||
XH("Xhosa"),
|
||||
YI("Yiddish"),
|
||||
YO("Yoruba"),
|
||||
ZA("Zhuang"),
|
||||
ZU("Zulu");
|
||||
|
||||
private final String name;
|
||||
private final String code;
|
||||
|
||||
Language(final String name) {
|
||||
this.name = name;
|
||||
this.code = name().toLowerCase();
|
||||
}
|
||||
|
||||
public static Language fromCode(final @NotNull String code) {
|
||||
return Language.valueOf(code.toUpperCase());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 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,102 +19,116 @@ package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
|
||||
import org.simpleyaml.configuration.MemorySection;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
|
||||
|
||||
@Getter
|
||||
public final class SimpleYamlConfig {
|
||||
private final File file;
|
||||
private final YamlFile yaml;
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
String.format("Running Sonar version %s on %s",
|
||||
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
|
||||
"Need help or have questions? https://jonesdev.xyz/discord",
|
||||
"https://github.com/jonesdevelopment/sonar",
|
||||
""
|
||||
);
|
||||
|
||||
public SimpleYamlConfig(final File dataFolder, final String fileName) {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
|
||||
public SimpleYamlConfig(final @NotNull File file) {
|
||||
this.file = file;
|
||||
this.yaml = new YamlFile(file.getPath());
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
yaml.save();
|
||||
} catch (IOException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
private void replaceConfigFile(final @NotNull URL defaultData) throws Exception {
|
||||
try (final InputStream inputStream = defaultData.openStream()) {
|
||||
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() throws Exception {
|
||||
// https://github.com/jonesdevelopment/sonar/issues/26
|
||||
// Only load the configuration if the file already exists
|
||||
if (yaml.exists()) {
|
||||
yaml.loadWithComments();
|
||||
} else {
|
||||
yaml.createOrLoadWithComments();
|
||||
public void load(final @NotNull URL defaultData) throws Exception {
|
||||
final boolean exists = yaml.exists();
|
||||
|
||||
// Copy the configuration from the translations
|
||||
if (!exists) {
|
||||
replaceConfigFile(defaultData);
|
||||
}
|
||||
|
||||
// Always load the comment format and options
|
||||
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
|
||||
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
|
||||
// Load the configuration
|
||||
yaml.loadWithComments();
|
||||
|
||||
// I hate my life... but this works... somehow
|
||||
if (exists) {
|
||||
// First, we store all values from the current (old) config
|
||||
final Map<String, Object> values = yaml.getValues(true);
|
||||
// Then, we replace the old config file with the default one
|
||||
replaceConfigFile(defaultData);
|
||||
// Re-load the configuration
|
||||
yaml.loadWithComments();
|
||||
// Loop through the new values from the new configuration
|
||||
yaml.getValues(true).forEach((path, v) -> {
|
||||
final Object value = values.get(path);
|
||||
// Make sure we actually need to update the value
|
||||
if (value != null && !value.equals(v)
|
||||
&& !(v instanceof MemorySection)
|
||||
&& !(value instanceof MemorySection)) {
|
||||
// Set the old values to the new values
|
||||
yaml.set(path, value);
|
||||
}
|
||||
});
|
||||
// Save the yaml file to ensure we keep the original values
|
||||
yaml.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void set(final String path, final Object v) {
|
||||
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());
|
||||
return 0;
|
||||
}
|
||||
return yaml.getInt(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path, final boolean def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getBoolean(path, def);
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return false;
|
||||
}
|
||||
return yaml.getBoolean(path);
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = yaml.get(path);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
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 List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+146
-95
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,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.JdbcPooledConnectionSource;
|
||||
import com.j256.ormlite.stmt.QueryBuilder;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import com.j256.ormlite.table.TableUtils;
|
||||
@@ -26,12 +28,13 @@ 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.dependencies.DependencyLoader;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
@@ -42,29 +45,79 @@ 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() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[database] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
|
||||
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) {
|
||||
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
|
||||
LOGGER.warn("Configure a database to save verified players.");
|
||||
return;
|
||||
}
|
||||
|
||||
try (final ConnectionSource connectionSource = DependencyLoader.setUpDriverAndConnect()) {
|
||||
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) {
|
||||
@@ -76,13 +129,55 @@ public final class VerifiedPlayerController {
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
// 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(() -> {
|
||||
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("Error initializing database: {}", exception);
|
||||
}
|
||||
});
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error setting up database: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection if instantiated
|
||||
*/
|
||||
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) 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();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
LOGGER.info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +213,7 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
|
||||
LOGGER.error("Error trying to remove entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -129,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +249,9 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
try {
|
||||
dao.create(player);
|
||||
dao.createIfNotExists(player);
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
|
||||
LOGGER.error("Error trying to add entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -167,79 +262,54 @@ 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();
|
||||
|
||||
// Only update the column if the database type is not NONE
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
dao.deleteBuilder().delete();
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
public void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
public synchronized void clearAll() {
|
||||
cache.clear();
|
||||
|
||||
// Delete the entire table from the database, if necessary
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
try {
|
||||
final long timestamp = Instant.now()
|
||||
.minus(maximumAge, ChronoUnit.DAYS)
|
||||
.getEpochSecond() * 1000L; // convert to ms
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
dao.deleteBuilder().delete();
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
|
||||
LOGGER.error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,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,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.dependencies;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
|
||||
public enum Dependency {
|
||||
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.2.0"
|
||||
),
|
||||
// TODO: MariaDB still seems to have some issues; investigate!
|
||||
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
|
||||
MARIADB(
|
||||
"org/mariadb/jdbc",
|
||||
"mariadb-java-client",
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
final @NotNull String version) {
|
||||
final String archiveFileName = String.format("%s-%s.jar", artifactId, version);
|
||||
this.tempFilePath = new File(Sonar.get().getDataDirectory(), "libraries/" + archiveFileName).toPath();
|
||||
|
||||
try {
|
||||
final String mvnCentralPath = String.format("%s/%s/%s/%s-%s.jar",
|
||||
groupId, artifactId, version, artifactId, version);
|
||||
|
||||
this.mvnRepoURL = new URL("https://repo1.maven.org/maven2/" + mvnCentralPath);
|
||||
} catch (MalformedURLException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL for the local file used for the external URLClassLoader
|
||||
*/
|
||||
public @NotNull URL getClassLoaderURL() throws MalformedURLException {
|
||||
if (!Files.exists(tempFilePath)) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Downloading dependency {}...", tempFilePath.getFileName());
|
||||
|
||||
try (final InputStream inputStream = mvnRepoURL.openStream()) {
|
||||
Files.createDirectories(tempFilePath.getParent());
|
||||
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
|
||||
}
|
||||
|
||||
return tempFilePath.toUri().toURL();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.dependencies;
|
||||
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Driver;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.Properties;
|
||||
|
||||
@UtilityClass
|
||||
public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
|
||||
final URL url = database.getType().getDependency().getClassLoaderURL();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
final Connection connection = connect(classLoader, jdbcURL, database);
|
||||
return new JdbcSingleConnectionSource(jdbcURL, connection);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration.Database database) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.put("user", database.getUsername());
|
||||
properties.put("password", database.getPassword());
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
return (Connection) connect.invoke(driver, databaseURL, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.dependencies;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
final class ExternalClassLoader extends URLClassLoader {
|
||||
|
||||
/**
|
||||
* Create a URLClassLoader with the system ClassLoader as parent
|
||||
*
|
||||
* @param url URL for of the local file
|
||||
*/
|
||||
public ExternalClassLoader(final URL[] url) {
|
||||
super(url, ClassLoader.getSystemClassLoader().getParent());
|
||||
}
|
||||
|
||||
// Register this URLClassLoader as parallel capable
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 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 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,9 +26,8 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+6
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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,9 +25,8 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,11 +27,10 @@ import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackUser user;
|
||||
private final long timeTakenToVerify;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 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,65 +17,70 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.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.TimeUnit;
|
||||
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());
|
||||
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L);
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
// 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 = 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 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,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_IN_TRAFFIC = "sonar-traffic-in";
|
||||
String FALLBACK_OUT_TRAFFIC = "sonar-traffic-out";
|
||||
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 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,50 +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 {
|
||||
public static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(16, 0.5f);
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,44 +17,49 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackRatelimiter {
|
||||
public static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
@Setter
|
||||
private ExpiringCache<InetAddress> 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(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.has(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress);
|
||||
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 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,68 +19,84 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
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.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser<X, Y> {
|
||||
@NotNull Fallback getFallback();
|
||||
public interface FallbackUser {
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull X getPlayer();
|
||||
@NotNull
|
||||
ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull Y getConnection();
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
|
||||
@NotNull Channel getChannel();
|
||||
@NotNull
|
||||
ProtocolVersion getProtocolVersion();
|
||||
|
||||
@NotNull ChannelPipeline getPipeline();
|
||||
@NotNull
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
@NotNull
|
||||
UUID getOfflineUuid();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
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 Legacy disconnect message string
|
||||
* @see #disconnect(Component)
|
||||
*/
|
||||
default void disconnect(final @NotNull String reason) {
|
||||
disconnect(Component.text(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
*
|
||||
* @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 offlineUuid UUID of the player
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
*
|
||||
* @param msg Message to send to the player
|
||||
*/
|
||||
void write(final @NotNull Object msg);
|
||||
default void write(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().writeAndFlush(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a buffered message that will be
|
||||
* sent once all messages are flushed.
|
||||
*/
|
||||
void delayedWrite(final @NotNull Object msg);
|
||||
|
||||
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
|
||||
default void delayedWrite(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().write(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
@@ -89,39 +105,8 @@ public interface FallbackUser<X, Y> {
|
||||
* 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) {
|
||||
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));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
|
||||
// Check if the player has too many failed attempts
|
||||
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
} else {
|
||||
// Cache the InetAddress for 3 minutes
|
||||
PREVIOUS_FAILS.put(getInetAddress().toString());
|
||||
}
|
||||
}
|
||||
void fail(final @NotNull String reason);
|
||||
}
|
||||
+6
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -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 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 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 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 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 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"};
|
||||
}
|
||||
}
|
||||
+11
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,20 +15,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand;
|
||||
package xyz.jonesdev.sonar.api.ormlite;
|
||||
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.common.subcommand.impl.*;
|
||||
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class SubcommandRegistryHolder implements SubcommandRegistry {
|
||||
public SubcommandRegistryHolder() {
|
||||
register(
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand()
|
||||
);
|
||||
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 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();
|
||||
}
|
||||
}
|
||||
@@ -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 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 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);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@Getter
|
||||
public final class Notification extends Observable {
|
||||
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
@Getter
|
||||
public abstract class Observable {
|
||||
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public abstract void observe();
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public final boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
return getSubscribers().contains(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
public final void subscribe(final UUID uuid) {
|
||||
getSubscribers().add(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
public final void unsubscribe(final UUID uuid) {
|
||||
getSubscribers().remove(uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,108 +18,73 @@
|
||||
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 xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
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.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements JVMProfiler {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
public final class Verbose extends Observable {
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
loginsPerSecond.cleanUp(false);
|
||||
|
||||
@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 = 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().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
* Copyright (C) 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,18 +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 = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
name = rootProject.name
|
||||
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", "ProtocolSupport",
|
||||
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
|
||||
apiVersion = "1.13" // ignore legacy plugin warning
|
||||
foliaSupported = true
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
@@ -27,18 +28,24 @@ 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.1")
|
||||
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")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bukkit:3.0.2")
|
||||
implementation(rootProject.libs.adventure.platform.bukkit) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bukkit)
|
||||
implementation(rootProject.libs.libby.bukkit)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,30 @@
|
||||
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
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, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,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.
|
||||
@@ -67,16 +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());
|
||||
}
|
||||
|
||||
// Let the injector know that the plugin has been enabled
|
||||
INITIALIZE_LISTENER.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,29 +1,33 @@
|
||||
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 = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
name = rootProject.name
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
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.1")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
implementation(rootProject.libs.adventure.platform.bungee) {
|
||||
exclude(module = "adventure-nbt")
|
||||
}
|
||||
implementation(rootProject.libs.bstats.bungee)
|
||||
implementation(rootProject.libs.libby.bungee)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,26 +17,27 @@
|
||||
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackBungeeInjector;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,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.
|
||||
@@ -68,22 +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 FallbackListener());
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackBungeeInjector.inject();
|
||||
}
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
@Override
|
||||
public void disable() {
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 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.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class BungeeInvocationSource extends InvocationSource {
|
||||
public BungeeInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(),
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender instanceof ProxiedPlayer,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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);
|
||||
}
|
||||
}
|
||||
+20
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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,34 +15,47 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
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 BaseInjectionHelper {
|
||||
public void inject(final ChannelInitializer<Channel> childHandler) {
|
||||
public class FallbackBungeeInjector {
|
||||
public void inject() {
|
||||
try {
|
||||
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
|
||||
childField.setAccessible(true);
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
|
||||
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
|
||||
new FallbackBungeeInboundHandler()));
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
|
||||
|
||||
// Get the base object (the object containing the field)
|
||||
final Object base = unsafe.staticFieldBase(childField);
|
||||
// Get the offset of the static field within its class
|
||||
final long offset = unsafe.staticFieldOffset(childField);
|
||||
|
||||
unsafe.putObject(base, offset, childHandler);
|
||||
// Replace the original channel initializer
|
||||
unsafe.putObject(base, offset, injectedInitializer);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.connection.DownstreamBridge;
|
||||
import net.md_5.bungee.connection.UpstreamBridge;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PacketHandler;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
private ChannelWrapper channel;
|
||||
private PacketHandler handler;
|
||||
|
||||
@Override
|
||||
public void setHandler(final @NotNull PacketHandler handler) {
|
||||
this.handler = handler;
|
||||
super.setHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
|
||||
try {
|
||||
handler.exception(throwable);
|
||||
} catch (Exception exception) {
|
||||
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
channel.markClosed();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
-345
@@ -1,345 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.EncryptionUtil;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new CorruptedFrameException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// The channel always stays open because the client sends
|
||||
// a StatusRequest and a Ping packet after one another
|
||||
if (!isConnected()) {
|
||||
throw new CorruptedFrameException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new CorruptedFrameException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
final int protocolId = getHandshake().getProtocolVersion();
|
||||
protocolVersion = ProtocolVersion.fromId(protocolId);
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
try {
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
return;
|
||||
}
|
||||
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
final int limit = bungee.config.getPlayerLimit();
|
||||
if (limit > 0 && bungee.getOnlineCount() >= limit) {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| user.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
if (cachedKickPacket == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (channelWrapper.getHandle().isActive()) {
|
||||
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
channelWrapper.getHandle().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
channelWrapper.getHandle().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
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;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
|
||||
private final Fallback fallback;
|
||||
private final ChannelWrapper player;
|
||||
private final FallbackInitialHandler connection;
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delayedWrite(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.write(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
|
||||
|
||||
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
|
||||
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
try {
|
||||
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
|
||||
} catch (ChannelException ignored) {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
|
||||
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
|
||||
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
|
||||
|
||||
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
|
||||
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
|
||||
private static final KickStringWriter LEGACY_KICK;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field kickField = PipelineUtils.class.getDeclaredField("legacyKicker");
|
||||
kickField.setAccessible(true);
|
||||
LEGACY_KICK = (KickStringWriter) kickField.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
|
||||
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
|
||||
channel.close();
|
||||
} else {
|
||||
try {
|
||||
BASE.initChannel(channel);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.varint;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
// MessageToMessageDecoder always do release after decoding
|
||||
// https://github.com/jonesdevelopment/sonar-legacy/pull/5
|
||||
return;
|
||||
}
|
||||
|
||||
final VarintByteDecoder reader = new VarintByteDecoder();
|
||||
final int end = byteBuf.forEachByte(reader);
|
||||
|
||||
if (end == -1) {
|
||||
// This is probably a good sign that the buffer was too short or empty
|
||||
// since the ByteBuf cannot hold a proper VarInt.
|
||||
if (reader.getResult() == VarintByteDecoder.DecoderResult.RUN_OF_ZEROES) {
|
||||
byteBuf.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reader.getResult()) {
|
||||
case RUN_OF_ZEROES: {
|
||||
// this will return to the point where the next varInt starts
|
||||
byteBuf.readerIndex(end);
|
||||
break;
|
||||
}
|
||||
|
||||
case SUCCESS: {
|
||||
final int readVarInt = reader.getReadVarInt();
|
||||
|
||||
if (readVarInt < 0) {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
} else if (readVarInt == 0) {
|
||||
// Actually, we don't want to throw an Exception if the packet is empty.
|
||||
// The check would also false flag a lot of legit players since packets
|
||||
// in 1.7 could sometimes be empty.
|
||||
byteBuf.readerIndex(end + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
final int bytesRead = reader.getBytesRead();
|
||||
final int minimumRead = bytesRead + readVarInt;
|
||||
|
||||
if (byteBuf.isReadable(minimumRead)) {
|
||||
out.add(byteBuf.retainedSlice(end + 1, readVarInt));
|
||||
|
||||
byteBuf.skipBytes(minimumRead);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
Loaded 100 of 257 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user