mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
+14
-32
@@ -1,12 +1,11 @@
|
||||
<div align="center">
|
||||
<!-- Introduction -->
|
||||
<p>
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server. Supporting every client and server version.
|
||||
<br>
|
||||
An effective and extensible solution for protecting your Minecraft server against all kinds of bot attacks
|
||||
</p>
|
||||
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server.
|
||||
<br>
|
||||
Sonar keeps your server safe from common and even sophisticated bot attacks.
|
||||
<br><br>
|
||||
|
||||
<!-- Badges & icons -->
|
||||
[](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
|
||||
[](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
|
||||
@@ -15,7 +14,6 @@
|
||||
[](https://jonesdev.xyz/discord)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
<br>
|
||||
<br>
|
||||
<!-- Quick navigation -->
|
||||
[Releases](https://github.com/jonesdevelopment/sonar/releases)
|
||||
|
|
||||
@@ -29,7 +27,6 @@
|
||||
</div>
|
||||
|
||||
## Design and Goal
|
||||
|
||||
* Effective, lightweight, and easy-to-use
|
||||
* No unnecessary features and clean code
|
||||
* Protection against all kinds of bot attacks
|
||||
@@ -38,44 +35,29 @@
|
||||
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
|
||||
|
||||
## Checks
|
||||
Sonar analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend. It is supposed to be an instant, powerful, and simple method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
|
||||
- Sonar queues new player logins to prevent spam login attacks
|
||||
- Sonar checks the handshake packets for legitimacy
|
||||
- Sonar makes sure some packets cannot be duplicated illegitimately
|
||||
1. Sonar sends the player to a lightweight fake server when they connect for the first time.
|
||||
2. Sonar verifies that players obey the laws of Minecraft's physics, including gravity and proper block collision.
|
||||
3. Sonar verifies that players send legitimate packets when interacting with vehicles (e.g. boats).
|
||||
4. Sonar makes sure that players send legitimate packets according to the [vanilla Minecraft protocol](<https://wiki.vg/Protocol>).
|
||||
|
||||
### Fallback
|
||||
|
||||
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend.
|
||||
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
* Sends the player to a lightweight fake server when they connect for the first time.
|
||||
* Analyzes if the player is sending the necessary packets.
|
||||
* Analyzes if the player is sending legitimate packets.
|
||||
* Checks if the player is obeying client gravity.
|
||||
* Checks if the player is colliding with blocks correctly.
|
||||
|
||||
Fallback also protects against huge spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
|
||||
Sonar also protects against spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
|
||||
|
||||
### False positives
|
||||
|
||||
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check. However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket on [Discord](https://jonesdev.xyz/discord/).
|
||||
Sonar is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
|
||||
If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues/new/choose) or a ticket on the [Discord server](https://jonesdev.xyz/discord/).
|
||||
|
||||
## Building
|
||||
|
||||
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/development/building).
|
||||
<br>
|
||||
You can also take a look at the [gradle documentation](https://docs.gradle.org/current/userguide/userguide.html) for a better understanding of Gradle.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you are interested in contributing, you can check out the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed instructions.
|
||||
|
||||
## License
|
||||
|
||||
Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
||||
|
||||
## Credits
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+12
-7
@@ -1,13 +1,9 @@
|
||||
import net.kyori.indra.git.IndraGitExtension
|
||||
|
||||
plugins {
|
||||
java
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.versioner) apply true
|
||||
}
|
||||
|
||||
versioner {
|
||||
pattern {
|
||||
pattern = "$version-%h-%c-%b"
|
||||
}
|
||||
alias(libs.plugins.indra.git) apply true
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -53,6 +49,7 @@ allprojects {
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
|
||||
relocate("org.h2", "xyz.jonesdev.sonar.libs.h2")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
@@ -69,10 +66,18 @@ allprojects {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.32
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
version=2.0.38
|
||||
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
|
||||
@@ -1,13 +1,13 @@
|
||||
[versions]
|
||||
netty = "4.1.110.Final"
|
||||
netty = "4.1.112.Final"
|
||||
adventure = "4.17.0"
|
||||
adventure-platform = "4.3.3"
|
||||
bstats = "3.0.2"
|
||||
libby = "2.0.0-SNAPSHOT"
|
||||
|
||||
[plugins]
|
||||
versioner = "io.toolebox.git-versioner:1.6.7"
|
||||
shadow = "io.github.goooler.shadow:8.1.7"
|
||||
indra-git = "net.kyori.indra.git:3.1.3"
|
||||
shadow = "io.github.goooler.shadow:8.1.8"
|
||||
|
||||
[libraries]
|
||||
netty = { module = "io.netty:netty-all", version.ref = "netty" }
|
||||
@@ -27,7 +27,7 @@ bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
|
||||
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
|
||||
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
|
||||
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
|
||||
lombok = "org.projectlombok:lombok:1.18.32"
|
||||
lombok = "org.projectlombok:lombok:1.18.34"
|
||||
capja = "xyz.jonesdev:capja:1.0.3"
|
||||
bungeecord = "net.md_5:bungeecord-proxy:master-SNAPSHOT"
|
||||
velocity = "com.velocitypowered:velocity-proxy:3.3.0-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.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-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/.
|
||||
@@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
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
|
||||
|
||||
Vendored
+2
@@ -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 ##########################################################################
|
||||
|
||||
@@ -89,7 +89,7 @@ public interface Sonar {
|
||||
|
||||
@NotNull
|
||||
default SonarVersion getVersion() {
|
||||
return SonarVersion.GET;
|
||||
return SonarVersion.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,7 +53,7 @@ public final class AttackTracker {
|
||||
public void checkIfUnderAttack() {
|
||||
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.
|
||||
@@ -68,7 +68,7 @@ public final class AttackTracker {
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().sendAttackNotification();
|
||||
Sonar.get().getNotificationHandler().observe();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -125,16 +125,16 @@ public final class AttackTracker {
|
||||
webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%peak-cps%", peakCPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
.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());
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,20 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Ticker;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.Tag;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -39,92 +36,65 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
default void handle(final @NotNull InvocationSource source, final String[] args) {
|
||||
if (source.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final long mapTimestamp = COMMAND_DELAY.get(source.getAudience(), audience -> -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
COMMAND_DELAY.put(source.getAudience(), timestamp);
|
||||
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
|| Arrays.stream(sub.getInfo().aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
subcommand.ifPresentOrElse(command -> {
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
subcommand.get().invoke(source, args);
|
||||
if (!command.getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(command.getPermission())) {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("permission", command.getPermission())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Invoke subcommand
|
||||
command.invoke(source, args);
|
||||
}, () -> {
|
||||
// Print standard help message
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>",
|
||||
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
|
||||
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
|
||||
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
|
||||
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
source.sendMessage(component);
|
||||
}
|
||||
final Component yes = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.tick"));
|
||||
final Component no = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.cross"));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
|
||||
TagResolver.resolver("suggest-subcommand",
|
||||
(queue, context) -> Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()))),
|
||||
Placeholder.unparsed("subcommand", command.getInfo().name()),
|
||||
Placeholder.unparsed("description", command.getDescription()),
|
||||
Placeholder.unparsed("permission", command.getPermission()),
|
||||
Placeholder.unparsed("aliases", command.getAliases()),
|
||||
Placeholder.component("only-players", command.getInfo().onlyPlayers() ? yes : no),
|
||||
Placeholder.component("only-console", command.getInfo().onlyConsole() ? yes : no)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
@@ -133,12 +103,10 @@ public interface SonarCommand {
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
ARG_TAB_SUGGESTIONS.put(alias, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,8 +115,8 @@ public interface SonarCommand {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
final String subcommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@@ -18,77 +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);
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
|
||||
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]))");
|
||||
|
||||
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);
|
||||
|
||||
+1
-8
@@ -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
|
||||
|
||||
+8
-14
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -88,11 +88,6 @@ public final class SimpleYamlConfig {
|
||||
yaml.set(path, v);
|
||||
}
|
||||
|
||||
public int getInt(final String path, final int def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getInt(path, def);
|
||||
}
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -101,11 +96,6 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getInt(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path, final boolean def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getBoolean(path, def);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -114,42 +104,18 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getBoolean(path);
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
LOGGER.info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = getObject(path);
|
||||
final Object object = yaml.get(path);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
throw new IllegalStateException("Invalid entry " + path);
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public Object getObject(final String path) {
|
||||
return yaml.get(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -158,11 +124,6 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
|
||||
@@ -21,12 +21,13 @@ import com.j256.ormlite.db.DatabaseType;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
@@ -45,6 +46,7 @@ import java.util.regex.Pattern;
|
||||
public final class SonarConfiguration {
|
||||
@Getter
|
||||
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
|
||||
@Getter
|
||||
private final File languageFile, pluginFolder;
|
||||
|
||||
static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
@@ -102,8 +104,11 @@ public final class SonarConfiguration {
|
||||
return Language.fromCode(property);
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.error("Could not find requested language: {}", throwable);
|
||||
LOGGER.error("You can view a full list of valid language codes here:");
|
||||
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
|
||||
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
} catch (IOException exception) {
|
||||
LOGGER.error("Error reading language file: {}", exception);
|
||||
}
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
@@ -160,6 +165,7 @@ public final class SonarConfiguration {
|
||||
|
||||
// Database
|
||||
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
|
||||
database.filename = generalConfig.getString("database.filename");
|
||||
database.host = generalConfig.getString("database.host");
|
||||
database.port = generalConfig.getInt("database.port");
|
||||
database.name = generalConfig.getString("database.name");
|
||||
@@ -225,19 +231,12 @@ public final class SonarConfiguration {
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.transfer.enabled = generalConfig.getBoolean("verification.transfer.enabled");
|
||||
verification.transfer.host = generalConfig.getString("verification.transfer.destination-host");
|
||||
verification.transfer.port = clamp(generalConfig.getInt("verification.transfer.destination-port"), 0, 0xffff);
|
||||
|
||||
if (verification.transfer.enabled) {
|
||||
Sonar.get().getLogger().info("Transferring 1.20.5+ clients is enabled. Please make sure to follow the instructions in order for this to work properly.");
|
||||
}
|
||||
|
||||
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
verification.logDuringAttack = generalConfig.getBoolean("verification.log-during-attack");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 500, 30000);
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 1000, 30000);
|
||||
verification.writeTimeout = clamp(generalConfig.getInt("verification.write-timeout"), 1000, 30000);
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay"), 0, 100000);
|
||||
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
|
||||
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
|
||||
@@ -260,8 +259,7 @@ public final class SonarConfiguration {
|
||||
|
||||
webhook.embed.title = webhookConfig.getString(embedPath + "title");
|
||||
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
|
||||
webhook.embed.description = formatString(String.join("\n",
|
||||
webhookConfig.getStringList(embedPath + "description")));
|
||||
webhook.embed.description = String.join("\n", webhookConfig.getStringList(embedPath + "description"));
|
||||
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
|
||||
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
|
||||
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
|
||||
@@ -277,87 +275,43 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
// Messages
|
||||
prefix = formatString(messagesConfig.getString("prefix"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
|
||||
header = fromList(messagesConfig.getStringList("header"));
|
||||
footer = fromList(messagesConfig.getStringList("footer"));
|
||||
tooManyOnlinePerIp = deserialize(fromList(messagesConfig.getStringList("too-many-online-per-ip")));
|
||||
|
||||
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
|
||||
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
|
||||
commands.invalidIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
|
||||
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
|
||||
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
|
||||
commands.commandCoolDownLeft = formatString(messagesConfig.getString("commands.cool-down-left"));
|
||||
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission"));
|
||||
commands.helpHeader = messagesConfig.getStringList("commands.main.header");
|
||||
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands"));
|
||||
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
|
||||
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
|
||||
commands.reloading = formatString(messagesConfig.getString("commands.reload.start"));
|
||||
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
header = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("header")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
footer = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("footer")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
commands.verboseSubscribed = formatString(messagesConfig.getString("commands.verbose.subscribed"));
|
||||
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed"));
|
||||
tooManyOnlinePerIp = deserializeDisconnectMessage("too-many-online-per-ip");
|
||||
verification.currentlyPreparing = deserializeDisconnectMessage("verification.currently-preparing");
|
||||
verification.tooFastReconnect = deserializeDisconnectMessage("verification.too-fast-reconnect");
|
||||
verification.alreadyVerifying = deserializeDisconnectMessage("verification.already-verifying");
|
||||
verification.alreadyQueued = deserializeDisconnectMessage("verification.already-queued");
|
||||
verification.blacklisted = deserializeDisconnectMessage("verification.blacklisted");
|
||||
verification.invalidUsername = deserializeDisconnectMessage("verification.invalid-username");
|
||||
verification.protocolBlacklisted = deserializeDisconnectMessage("verification.blacklisted-protocol");
|
||||
verification.verificationSuccess = deserializeDisconnectMessage("verification.success");
|
||||
verification.verificationFailed = deserializeDisconnectMessage("verification.failed");
|
||||
|
||||
commands.notificationsSubscribed = formatString(messagesConfig.getString("commands.notify.subscribed"));
|
||||
commands.notificationsUnsubscribed = formatString(messagesConfig.getString("commands.notify.unsubscribed"));
|
||||
verboseAnimation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
}
|
||||
|
||||
commands.blacklistEmpty = formatString(messagesConfig.getString("commands.blacklist.empty"));
|
||||
commands.blacklistCleared = formatString(messagesConfig.getString("commands.blacklist.cleared"));
|
||||
commands.blacklistSize = formatString(messagesConfig.getString("commands.blacklist.size"));
|
||||
commands.blacklistAdd = formatString(messagesConfig.getString("commands.blacklist.added"));
|
||||
commands.blacklistAddWarning = formatString(messagesConfig.getString("commands.blacklist.added-warning"));
|
||||
commands.blacklistRemove = formatString(messagesConfig.getString("commands.blacklist.removed"));
|
||||
commands.blacklistDuplicate = formatString(messagesConfig.getString("commands.blacklist.duplicate-ip"));
|
||||
commands.blacklistNotFound = formatString(messagesConfig.getString("commands.blacklist.ip-not-found"));
|
||||
|
||||
commands.verifiedEmpty = formatString(messagesConfig.getString("commands.verified.empty"));
|
||||
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
|
||||
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
|
||||
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
|
||||
commands.verifiedAdd = formatString(messagesConfig.getString("commands.verified.added"));
|
||||
commands.verifiedAlready = formatString(messagesConfig.getString("commands.verified.already"));
|
||||
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
|
||||
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
|
||||
|
||||
commands.statisticsHeader = formatString(messagesConfig.getString("commands.statistics.header"));
|
||||
commands.unknownStatisticType = formatString(messagesConfig.getString("commands.statistics.unknown-type"));
|
||||
commands.generalStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.general")));
|
||||
commands.cpuStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.cpu")));
|
||||
commands.memoryStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.memory")));
|
||||
commands.networkStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.network")));
|
||||
|
||||
verification.connectLog = formatString(messagesConfig.getString("verification.logs.connection"));
|
||||
verification.failedLog = formatString(messagesConfig.getString("verification.logs.failed"));
|
||||
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
|
||||
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
|
||||
|
||||
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
|
||||
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
|
||||
verification.tooFastReconnect = deserialize(fromList(messagesConfig.getStringList("verification.too-fast-reconnect")));
|
||||
verification.alreadyVerifying = deserialize(fromList(messagesConfig.getStringList("verification.already-verifying")));
|
||||
verification.alreadyQueued = deserialize(fromList(messagesConfig.getStringList("verification.already-queued")));
|
||||
verification.blacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted")));
|
||||
verification.invalidUsername = deserialize(fromList(messagesConfig.getStringList("verification.invalid-username")));
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol")));
|
||||
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
|
||||
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
|
||||
|
||||
verbose.actionBarLayout = deserialize(formatString(messagesConfig.getString("verbose.layout.normal")));
|
||||
verbose.actionBarLayoutDuringAttack = deserialize(formatString(messagesConfig.getString("verbose.layout.attack")));
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
|
||||
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title")));
|
||||
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle")));
|
||||
notifications.title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
notifications.notificationChat = deserialize(formatString(fromList(messagesConfig.getStringList("notifications.chat"))));
|
||||
private @NotNull Component deserializeDisconnectMessage(final String path) {
|
||||
return MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList(path)),
|
||||
Placeholder.component("prefix", prefix),
|
||||
Placeholder.component("header", header),
|
||||
Placeholder.component("footer", footer),
|
||||
Placeholder.unparsed("support-url", supportUrl));
|
||||
}
|
||||
|
||||
private @NotNull URL getAsset(final String url, final @NotNull Language language) {
|
||||
@@ -382,26 +336,6 @@ public final class SonarConfiguration {
|
||||
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list) {
|
||||
return formatString(String.join("<newline>", list));
|
||||
}
|
||||
|
||||
private static @NotNull Component deserialize(final String legacy) {
|
||||
return MiniMessage.miniMessage().deserialize(legacy);
|
||||
}
|
||||
|
||||
private @NotNull String formatString(final @NotNull String str) {
|
||||
return str
|
||||
.replace("%prefix%", prefix == null ? "" : prefix)
|
||||
.replace("%support-url%", supportUrl == null ? "" : supportUrl)
|
||||
.replace("%header%", header == null ? "" : header)
|
||||
.replace("%footer%", footer == null ? "" : footer);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final Verbose verbose = new Verbose();
|
||||
@Getter
|
||||
private final Commands commands = new Commands();
|
||||
@Getter
|
||||
private final Queue queue = new Queue();
|
||||
@Getter
|
||||
@@ -410,15 +344,14 @@ public final class SonarConfiguration {
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
@Getter
|
||||
private final Notifications notifications = new Notifications();
|
||||
|
||||
@Getter
|
||||
private String prefix;
|
||||
private Component prefix;
|
||||
private String supportUrl;
|
||||
private Component header;
|
||||
private Component footer;
|
||||
@Getter
|
||||
private String noPermission;
|
||||
private String header, footer;
|
||||
private Component noPermission;
|
||||
private boolean logPlayerAddresses;
|
||||
@Getter
|
||||
private int maxOnlinePerIp;
|
||||
@@ -432,23 +365,8 @@ public final class SonarConfiguration {
|
||||
private int attackCooldownDelay;
|
||||
@Getter
|
||||
private Component tooManyOnlinePerIp;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private Component actionBarLayout;
|
||||
private Component actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Notifications {
|
||||
private Title title;
|
||||
private Component notificationTitle;
|
||||
private Component notificationSubtitle;
|
||||
private Component notificationChat;
|
||||
}
|
||||
private List<String> verboseAnimation;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -475,7 +393,6 @@ public final class SonarConfiguration {
|
||||
private final Gravity gravity = new Gravity();
|
||||
private final Vehicle vehicle = new Vehicle();
|
||||
private final Brand brand = new Brand();
|
||||
private final Transfer transfer = new Transfer();
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
@@ -489,8 +406,6 @@ public final class SonarConfiguration {
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
|
||||
private Smear distortion = new Smear();
|
||||
|
||||
@@ -537,26 +452,16 @@ public final class SonarConfiguration {
|
||||
private Pattern validRegex;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Transfer {
|
||||
private boolean enabled;
|
||||
private String host;
|
||||
private int port;
|
||||
}
|
||||
|
||||
private boolean checkGeyser;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
private Pattern validNameRegex;
|
||||
private Pattern validLocaleRegex;
|
||||
private String connectLog;
|
||||
private String failedLog;
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int readTimeout;
|
||||
private int writeTimeout;
|
||||
private int reconnectDelay;
|
||||
private int rememberTime;
|
||||
private int blacklistTime;
|
||||
@@ -575,55 +480,6 @@ public final class SonarConfiguration {
|
||||
private Component currentlyPreparing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Commands {
|
||||
private String incorrectCommandUsage;
|
||||
private String invalidIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
private String commandCoolDown;
|
||||
private String commandCoolDownLeft;
|
||||
|
||||
private String blacklistEmpty;
|
||||
private String blacklistAdd;
|
||||
private String blacklistAddWarning;
|
||||
private String blacklistDuplicate;
|
||||
private String blacklistNotFound;
|
||||
private String blacklistRemove;
|
||||
private String blacklistCleared;
|
||||
private String blacklistSize;
|
||||
|
||||
private String verifiedRemove;
|
||||
private String verifiedAdd;
|
||||
private String verifiedAlready;
|
||||
private String verifiedNotFound;
|
||||
private String verifiedCleared;
|
||||
private String verifiedSize;
|
||||
private String verifiedEmpty;
|
||||
private String verifiedBlocked;
|
||||
|
||||
private String statisticsHeader;
|
||||
private String unknownStatisticType;
|
||||
private String generalStatistics;
|
||||
private String memoryStatistics;
|
||||
private String networkStatistics;
|
||||
private String cpuStatistics;
|
||||
|
||||
private List<String> helpHeader;
|
||||
private String helpSubcommands;
|
||||
|
||||
private String verboseSubscribed;
|
||||
private String verboseUnsubscribed;
|
||||
|
||||
private String notificationsSubscribed;
|
||||
private String notificationsUnsubscribed;
|
||||
|
||||
private String reloading;
|
||||
private String reloaded;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Database {
|
||||
@@ -633,6 +489,7 @@ public final class SonarConfiguration {
|
||||
public enum Type {
|
||||
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
|
||||
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
|
||||
H2("jdbc:h2:file:%s", new H2DatabaseTypeAdapter()),
|
||||
NONE(null, null);
|
||||
|
||||
private final String connectionString;
|
||||
@@ -640,6 +497,7 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
private Type type;
|
||||
private String filename;
|
||||
private String host;
|
||||
private int port;
|
||||
private String name;
|
||||
|
||||
+14
-17
@@ -33,7 +33,7 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
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;
|
||||
@@ -82,16 +82,24 @@ public final class VerifiedPlayerController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize MySQL/MariaDB driver
|
||||
database.getType().getDatabaseType().loadDriver();
|
||||
|
||||
try {
|
||||
connectionSource = new JdbcPooledConnectionSource(
|
||||
String.format(database.getType().getConnectionString(),
|
||||
database.getHost(), database.getPort(), database.getName()),
|
||||
String jdbcURL = String.format(database.getType().getConnectionString(),
|
||||
database.getHost(), database.getPort(), database.getName());
|
||||
|
||||
// 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().getDatabase().getFilename());
|
||||
jdbcURL = String.format(database.getType().getConnectionString(), file.getAbsolutePath());
|
||||
}
|
||||
|
||||
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
|
||||
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
|
||||
|
||||
// Create table
|
||||
// Create database table
|
||||
try {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException exception) {
|
||||
@@ -127,7 +135,6 @@ public final class VerifiedPlayerController {
|
||||
// The connection source will always be null if the database type is NONE.
|
||||
if (connectionSource != null) {
|
||||
try {
|
||||
// Properly close the connection
|
||||
connectionSource.close();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error("Error closing database: {}", exception);
|
||||
@@ -293,16 +300,6 @@ public final class VerifiedPlayerController {
|
||||
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
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class FallbackQueue {
|
||||
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);
|
||||
|
||||
private final ConcurrentMap<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(
|
||||
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
|
||||
128, 0.75f, Runtime.getRuntime().availableProcessors());
|
||||
|
||||
public void poll() {
|
||||
@@ -46,7 +46,7 @@ public final class FallbackQueue {
|
||||
|
||||
// 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 = queuedPlayers.entrySet().iterator();
|
||||
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();
|
||||
|
||||
@@ -21,7 +21,6 @@ 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.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
@@ -60,10 +59,8 @@ public interface FallbackUser {
|
||||
* using our custom Disconnect packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
* @param duringLogin True, if the player has not joined yet
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void disconnect(final @NotNull Component reason, final boolean duringLogin);
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
|
||||
+7
-21
@@ -20,10 +20,8 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -69,7 +67,8 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765),
|
||||
MINECRAFT_1_20_5(766);
|
||||
MINECRAFT_1_20_5(766),
|
||||
MINECRAFT_1_21(767);
|
||||
|
||||
private final int protocol;
|
||||
|
||||
@@ -77,27 +76,14 @@ public enum ProtocolVersion {
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
|
||||
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()) {
|
||||
versions.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -77,14 +77,6 @@ public class JVMProcessInformation {
|
||||
return getSystemCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current system load average in percent
|
||||
* @apiNote This returns the global process load, not the CPU usage
|
||||
*/
|
||||
public double getSystemLoadAverage() {
|
||||
return MX.getSystemLoadAverage() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM will attempt to use
|
||||
*/
|
||||
|
||||
@@ -63,7 +63,13 @@ public final class VerifiedPlayer {
|
||||
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this.inetAddress = inetAddress.toString();
|
||||
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.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
|
||||
+10
-14
@@ -15,21 +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.H2DatabaseType;
|
||||
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(),
|
||||
new NotifyCommand()
|
||||
);
|
||||
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"};
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -22,11 +22,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
|
||||
|
||||
/**
|
||||
* We need to override the default driver class name
|
||||
* in order to use the custom relocated MariaDB driver
|
||||
* Read more: {@link MysqlDatabaseTypeAdapter}
|
||||
*/
|
||||
// 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"};
|
||||
|
||||
+2
-5
@@ -22,11 +22,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
|
||||
|
||||
/**
|
||||
* We need to override the default driver class name
|
||||
* in order to use the custom relocated MySQL driver
|
||||
* Read more: {@link MariaDbDatabaseTypeAdapter}
|
||||
*/
|
||||
// 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"};
|
||||
|
||||
@@ -20,33 +20,70 @@ package xyz.jonesdev.sonar.api.verbose;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
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.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable {
|
||||
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
public final class Notification extends Observable {
|
||||
|
||||
public void sendAttackNotification() {
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
Component chat = null;
|
||||
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;
|
||||
|
||||
// Only prepare component if there are subscribers
|
||||
if (chat == null) {
|
||||
chat = replaceStatistic(Sonar.get().getConfig().getNotifications().getNotificationChat());
|
||||
}
|
||||
// Send the message to all online subscribers
|
||||
audience.sendMessage(chat);
|
||||
for (Component component : chatNotificationComponents) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
// Send the title to all online subscribers
|
||||
audience.showTitle(Sonar.get().getConfig().getNotifications().getTitle());
|
||||
audience.showTitle(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,97 +17,38 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
@Getter
|
||||
public abstract class Observable {
|
||||
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public interface Observable {
|
||||
Collection<UUID> getSubscribers();
|
||||
public abstract void observe();
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
default boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
public final boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
return getSubscribers().contains(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void subscribe(final UUID uuid) {
|
||||
public final void subscribe(final UUID uuid) {
|
||||
getSubscribers().add(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void unsubscribe(final UUID uuid) {
|
||||
public final void unsubscribe(final UUID uuid) {
|
||||
getSubscribers().remove(uuid);
|
||||
}
|
||||
|
||||
// TODO: Make this a bit prettier and better for performance
|
||||
default @NotNull Component replaceStatistic(final @NotNull Component component) {
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
return component
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%queued%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verifying%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%blacklisted%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-joins%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%logins-per-second%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%connections-per-second%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-total%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-success%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalSuccessfulVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-failed%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic%")
|
||||
.replacement(statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic%")
|
||||
.replacement(statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic-ttl%")
|
||||
.replacement(statistics.getTotalIncomingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic-ttl%")
|
||||
.replacement(statistics.getTotalOutgoingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%used-memory%")
|
||||
.replacement(formatMemory(getUsedMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%free-memory%")
|
||||
.replacement(formatMemory(getFreeMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-memory%")
|
||||
.replacement(formatMemory(getTotalMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%max-memory%")
|
||||
.replacement(formatMemory(getMaxMemory()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -20,52 +20,70 @@ 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.TextReplacementConfig;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements Observable {
|
||||
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
public final class Verbose extends Observable {
|
||||
private int animationIndex;
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
Component component = null;
|
||||
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 UUID subscriber : subscribers) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
|
||||
// Only prepare component if there are subscribers
|
||||
if (component == null) {
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
component = replaceStatistic(attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%attack-duration%")
|
||||
.replacement(attackDuration == null ? "00:00" : DATE_FORMATTER.format(attackDuration.delay()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%animation%")
|
||||
.replacement(nextAnimation())
|
||||
.build());
|
||||
}
|
||||
// Send the action bar to all online subscribers
|
||||
audience.sendActionBar(component);
|
||||
audience.sendActionBar(actionBarComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public String nextAnimation() {
|
||||
final var 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];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ plugins {
|
||||
bukkit {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
|
||||
@@ -5,7 +5,7 @@ plugins {
|
||||
bungee {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackBungeeInjector;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.UUID;
|
||||
@@ -96,7 +96,7 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject();
|
||||
FallbackBungeeInjector.inject();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+15
-7
@@ -29,23 +29,24 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
|
||||
public FallbackChannelHandler(final @NotNull Channel channel) {
|
||||
public FallbackBungeeChannelHandler(final @NotNull Channel channel) {
|
||||
super(channel);
|
||||
}
|
||||
|
||||
private static final Field CHANNEL_WRAPPER_FIELD;
|
||||
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
|
||||
CHANNEL_WRAPPER_FIELD.setAccessible(true);
|
||||
CHANNEL_WRAPPER_GETTER = MethodHandles.privateLookupIn(HandlerBoss.class, MethodHandles.lookup())
|
||||
.findGetter(HandlerBoss.class, "channel", ChannelWrapper.class);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
@@ -53,6 +54,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// TODO: put this into a separate handler
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (listenForPackets && msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
@@ -69,10 +71,16 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
// 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 = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
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();
|
||||
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
|
||||
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
packetWrapper.trySingleRelease();
|
||||
return;
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -23,11 +23,15 @@ 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.common.fallback.injection.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_HANDLER;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackInjectionHelper {
|
||||
public class FallbackBungeeInjector {
|
||||
public void inject() {
|
||||
try {
|
||||
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
|
||||
@@ -35,7 +39,9 @@ public class FallbackInjectionHelper {
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackInjectedChannelInitializer(
|
||||
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_HANDLER,
|
||||
new FallbackBungeeChannelHandler(pipeline.channel())));
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
@@ -61,15 +61,22 @@ class LibraryLoader {
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("8.3.0")
|
||||
.version("8.4.0")
|
||||
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
|
||||
.build(),
|
||||
// MariaDB driver
|
||||
Library.builder()
|
||||
.groupId("org{}mariadb{}jdbc")
|
||||
.artifactId("mariadb-java-client")
|
||||
.version("3.3.3")
|
||||
.version("3.4.0")
|
||||
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
|
||||
.build(),
|
||||
// H2 driver
|
||||
Library.builder()
|
||||
.groupId("com{}h2database")
|
||||
.artifactId("h2")
|
||||
.version("2.1.214")
|
||||
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
|
||||
.build()
|
||||
);
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceThreadManager;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.subcommands.*;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
import java.io.File;
|
||||
@@ -76,14 +76,23 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
this.subcommandRegistry = new SubcommandRegistry();
|
||||
// Register all subcommands
|
||||
this.subcommandRegistry.register(
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand(),
|
||||
new NotifyCommand());
|
||||
// Hide unnecessary debug information
|
||||
Logger.setGlobalLogLevel(Level.WARNING);
|
||||
}
|
||||
|
||||
public final void initialize() {
|
||||
// Check if the branch is not the main branch to warn about unstable versions
|
||||
if (!getVersion().isOnMainBranch()) {
|
||||
if (!getVersion().getGitBranch().equals("main")) {
|
||||
getLogger().warn("You are currently using an unofficial experimental branch.");
|
||||
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
|
||||
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
@@ -104,7 +113,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
// Start threads
|
||||
getLogger().info("Starting all managed threads...");
|
||||
SonarServiceThreadManager.start();
|
||||
SonarServiceManager.start();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
@@ -115,8 +124,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
return; // Do not check for updates if the launch failed
|
||||
}
|
||||
|
||||
// Check if a new version has been released
|
||||
UpdateChecker.checkForUpdates();
|
||||
// Check if a new version has been released if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
|
||||
UpdateChecker.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void enable();
|
||||
@@ -173,10 +184,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
}
|
||||
|
||||
public final void shutdown() {
|
||||
// Initialize shutdown process
|
||||
// Initialize the shutdown process
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// Interrupt threads
|
||||
SonarServiceThreadManager.stop();
|
||||
SonarServiceManager.stop();
|
||||
// Close database connection if present
|
||||
if (verifiedPlayerController != null) {
|
||||
verifiedPlayerController.close();
|
||||
|
||||
+21
-28
@@ -54,8 +54,6 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
public final void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
GlobalSonarStatistics.countConnection();
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
@@ -66,18 +64,11 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// Account for this by checking if the inetAddress has been set yet
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the connected players
|
||||
FALLBACK.getConnected().remove(inetAddress);
|
||||
FALLBACK.getConnected().compute(inetAddress, (key, value) -> null);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().getQueuedPlayers().remove(inetAddress);
|
||||
// Remove this account from the online players
|
||||
// or decrement the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (key, value) -> {
|
||||
if (value == null || value <= 1) {
|
||||
return null;
|
||||
} else {
|
||||
return value - 1;
|
||||
}
|
||||
});
|
||||
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> null);
|
||||
// Remove this account from the online players or decrement the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null || value <= 1 ? null : value - 1);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
@@ -110,6 +101,8 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +143,18 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player with the same IP address
|
||||
if (FALLBACK.getConnected().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
@@ -163,18 +168,6 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player with the same IP address
|
||||
if (FALLBACK.getConnected().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
@@ -185,7 +178,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + username;
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString(), offlineUUID)) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +197,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().getQueuedPlayers().put(inetAddress, () -> {
|
||||
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> () -> {
|
||||
// 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()
|
||||
@@ -239,7 +232,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
|
||||
if (onlinePerIp > maxOnlinePerIp) {
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
customDisconnect(channel, protocolVersion, tooManyOnlinePerIP, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-8
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,18 +25,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
|
||||
super(timeout, 0L, 0L, timeUnit);
|
||||
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
|
||||
super(readTimeout, writeTimeout, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
// We are only checking the read timeout state - skip all other states
|
||||
if (idleStateEvent.state() != IdleState.READER_IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
|
||||
+38
-37
@@ -53,7 +53,7 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress"})
|
||||
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
|
||||
public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
@@ -78,7 +78,9 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason, final boolean duringLogin) {
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
|
||||
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
|
||||
}
|
||||
|
||||
@@ -94,29 +96,32 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
// We let the user override this through the configuration.
|
||||
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
|
||||
.replace("<username>", username)
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
Sonar.get().getFallback().getConnected().put(inetAddress, (byte) 0);
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
// Mark the player as connected by caching them in a map of verifying players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (key, value) -> (byte) 0);
|
||||
|
||||
channel.eventLoop().execute(() -> {
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(
|
||||
Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
Sonar.get().getConfig().getVerification().getWriteTimeout(),
|
||||
TimeUnit.MILLISECONDS));
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (pipeline.get(handler) != null) {
|
||||
pipeline.remove(handler);
|
||||
@@ -128,23 +133,17 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
|
||||
|
||||
try {
|
||||
// Replace normal decoder to allow custom packets
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
|
||||
} catch (Throwable throwable) {
|
||||
// This rarely happens when the channel hangs and the player is still connecting
|
||||
// I honestly have no idea how else I'm supposed to fix this
|
||||
channel.close();
|
||||
}
|
||||
// Replace normal decoder to allow custom packets
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(final @NotNull String reason) {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed(), false);
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
// Only log the failed message if the server isn't currently under attack.
|
||||
// However, we let the user override this through the configuration.
|
||||
@@ -152,10 +151,11 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("<reason>", reason));
|
||||
}
|
||||
|
||||
// Increment number of total failed verifications
|
||||
@@ -189,9 +189,10 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
}
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
@@ -253,7 +254,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (channel.pipeline().get(boss) != null) {
|
||||
if (channel.pipeline().context(boss) != null) {
|
||||
channel.pipeline().remove(boss);
|
||||
}
|
||||
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
|
||||
|
||||
+8
-7
@@ -15,22 +15,21 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
package xyz.jonesdev.sonar.common.fallback.injection;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public final class FallbackInjectedChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final MethodHandle INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
@@ -43,6 +42,7 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
|
||||
}
|
||||
|
||||
private final ChannelInitializer<Channel> originalChannelInitializer;
|
||||
private final Consumer<ChannelPipeline> sonarPipelineInjector;
|
||||
|
||||
@Override
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
@@ -53,8 +53,9 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
if (channel.pipeline().get(PACKET_DECODER) != null) {
|
||||
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
|
||||
// Inject Sonar's channel handler into the pipeline
|
||||
if (channel.isActive()) {
|
||||
sonarPipelineInjector.accept(channel.pipeline());
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-19
@@ -52,26 +52,23 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
// Release the ByteBuf if the connection is not active
|
||||
// or the ByteBuf doesn't contain any data to avoid
|
||||
// memory leaks or other potential exploits.
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
final int originalReaderIndex = byteBuf.readerIndex();
|
||||
// Read the packet ID and then create the packet from it
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// If the packet isn't found, skip it
|
||||
if (packet == null) {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Release the ByteBuf if the connection is not active
|
||||
// or the ByteBuf doesn't contain any data to avoid
|
||||
// memory leaks or other potential exploits.
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the packet ID and then create the packet from it
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the packet isn't too large or too small
|
||||
checkPacketSize(byteBuf, packet);
|
||||
|
||||
|
||||
+8
-4
@@ -20,6 +20,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
@@ -29,15 +30,18 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
@Getter
|
||||
private FallbackPacketRegistry packetRegistry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry protocolRegistry;
|
||||
|
||||
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
updateRegistry(LOGIN);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry packetRegistry) {
|
||||
this.packetRegistry = packetRegistry;
|
||||
this.protocolRegistry = packetRegistry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,7 +50,7 @@ public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPa
|
||||
final ByteBuf out) throws Exception {
|
||||
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
|
||||
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
|
||||
final int packetId = registry.getPacketId(originalPacket);
|
||||
final int packetId = protocolRegistry.getPacketId(originalPacket);
|
||||
writeVarInt(out, packetId);
|
||||
packet.encode(out, protocolVersion);
|
||||
}
|
||||
|
||||
+44
-13
@@ -64,7 +64,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x05, MINECRAFT_1_20_2, true),
|
||||
map(0x07, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false),
|
||||
@@ -124,7 +124,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1A, MINECRAFT_1_19_4, true),
|
||||
map(0x1B, MINECRAFT_1_20_2, true),
|
||||
map(0x1D, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
|
||||
clientbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
map(0x08, MINECRAFT_1_7_2, true),
|
||||
map(0x2E, MINECRAFT_1_9, true),
|
||||
map(0x2F, MINECRAFT_1_12_1, true),
|
||||
@@ -140,7 +140,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3C, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_20_2, true),
|
||||
map(0x40, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
|
||||
clientbound.register(PlayerAbilitiesPacket.class, PlayerAbilitiesPacket::new,
|
||||
map(0x39, MINECRAFT_1_7_2, true),
|
||||
map(0x2B, MINECRAFT_1_9, true),
|
||||
map(0x2C, MINECRAFT_1_12_1, true),
|
||||
@@ -253,7 +253,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true),
|
||||
map(0x35, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
|
||||
clientbound.register(SetDefaultSpawnPositionPacket.class, SetDefaultSpawnPositionPacket::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
map(0x45, MINECRAFT_1_12, true),
|
||||
@@ -284,7 +284,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true),
|
||||
map(0x2C, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
|
||||
clientbound.register(SetContainerSlotPacket.class, SetContainerSlotPacket::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
map(0x17, MINECRAFT_1_13, true),
|
||||
@@ -312,7 +312,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x58, MINECRAFT_1_20_2, true),
|
||||
map(0x5A, MINECRAFT_1_20_3, true),
|
||||
map(0x5C, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
clientbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_13, false),
|
||||
@@ -331,7 +331,37 @@ public enum FallbackPacketRegistry {
|
||||
map(0x22, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(TransferPacket.class, TransferPacket::new,
|
||||
map(0x73, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, true),
|
||||
map(0x37, MINECRAFT_1_9, true),
|
||||
map(0x39, MINECRAFT_1_12, true),
|
||||
map(0x3A, MINECRAFT_1_12_1, true),
|
||||
map(0x3D, MINECRAFT_1_13, true),
|
||||
map(0x3F, MINECRAFT_1_14, true),
|
||||
map(0x40, MINECRAFT_1_15, true),
|
||||
map(0x3F, MINECRAFT_1_16, true),
|
||||
map(0x48, MINECRAFT_1_17, true),
|
||||
map(0x47, MINECRAFT_1_19, true),
|
||||
map(0x4A, MINECRAFT_1_19_1, true),
|
||||
map(0x49, MINECRAFT_1_19_3, true),
|
||||
map(0x4D, MINECRAFT_1_19_4, true),
|
||||
map(0x4F, MINECRAFT_1_20_2, true),
|
||||
map(0x51, MINECRAFT_1_20_3, true),
|
||||
map(0x53, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, false),
|
||||
map(0x17, MINECRAFT_1_9, false),
|
||||
map(0x1A, MINECRAFT_1_12, false),
|
||||
map(0x21, MINECRAFT_1_13, false),
|
||||
map(0x23, MINECRAFT_1_14, false),
|
||||
map(0x24, MINECRAFT_1_16, false),
|
||||
map(0x25, MINECRAFT_1_16_2, false),
|
||||
map(0x27, MINECRAFT_1_19, false),
|
||||
map(0x28, MINECRAFT_1_19_1, false),
|
||||
map(0x2B, MINECRAFT_1_20_2, false),
|
||||
map(0x2C, MINECRAFT_1_20_3, false),
|
||||
map(0x2F, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
|
||||
map(0x11, MINECRAFT_1_9, false),
|
||||
map(0x12, MINECRAFT_1_12, false),
|
||||
@@ -362,7 +392,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x22, MINECRAFT_1_20_2, false),
|
||||
map(0x23, MINECRAFT_1_20_3, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
map(0x03, MINECRAFT_1_12, false),
|
||||
@@ -388,7 +418,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false),
|
||||
map(0x18, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
map(0x05, MINECRAFT_1_12, false),
|
||||
@@ -412,8 +442,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x0D, MINECRAFT_1_19_1, false),
|
||||
map(0x0C, MINECRAFT_1_19_3, false),
|
||||
map(0x0D, MINECRAFT_1_19_4, false),
|
||||
map(0x0F, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
|
||||
map(0x0F, MINECRAFT_1_20_2, false),
|
||||
map(0x12, MINECRAFT_1_21, false));
|
||||
serverbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
map(0x06, MINECRAFT_1_7_2, false),
|
||||
map(0x0D, MINECRAFT_1_9, false),
|
||||
map(0x0F, MINECRAFT_1_12, false),
|
||||
@@ -429,9 +460,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false),
|
||||
map(0x1B, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
|
||||
serverbound.register(ConfirmTeleportationPacket.class, ConfirmTeleportationPacket::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(PlayerPositionPacket.class, PlayerPositionPacket::new,
|
||||
serverbound.register(SetPlayerPositionPacket.class, SetPlayerPositionPacket::new,
|
||||
map(0x04, MINECRAFT_1_7_2, false),
|
||||
map(0x0C, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_12, false),
|
||||
@@ -447,7 +478,7 @@ public enum FallbackPacketRegistry {
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false),
|
||||
map(0x1A, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PlayerPositionIdlePacket.class, PlayerPositionIdlePacket::new,
|
||||
serverbound.register(SetPlayerOnGround.class, SetPlayerOnGround::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0D, MINECRAFT_1_12, false),
|
||||
|
||||
+37
-17
@@ -18,6 +18,8 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
@@ -28,6 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishC
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
@@ -38,9 +41,9 @@ public class FallbackPreparer {
|
||||
// LoginSuccess
|
||||
public final FallbackPacket LOGIN_SUCCESS = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L), "Sonar"));
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new ClientAbilitiesPacket(0x00, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new ClientAbilitiesPacket(0x02, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new ClientAbilitiesPacket(0x06, 0f, 0f));
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x06, 0f, 0f));
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
|
||||
// Finish Configuration
|
||||
@@ -48,6 +51,7 @@ public class FallbackPreparer {
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC_LEGACY = new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null));
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
|
||||
// Game Event (1.20.3+)
|
||||
@@ -92,10 +96,8 @@ public class FallbackPreparer {
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
|
||||
|
||||
// Captcha position
|
||||
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new PlayerPositionLookPacket(
|
||||
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false));
|
||||
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION));
|
||||
|
||||
// Platform
|
||||
public BlockType blockType = BlockType.BARRIER;
|
||||
@@ -109,13 +111,18 @@ public class FallbackPreparer {
|
||||
// XP packets
|
||||
public FallbackPacket[] xpCountdown;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void prepare() {
|
||||
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
|
||||
Sonar.get().getLogger().info("Preloading all registered packets...");
|
||||
FallbackPacketRegistry.values();
|
||||
|
||||
// Prepare JoinGame packet
|
||||
joinGame = new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
0, false, 0,
|
||||
true, false, false,
|
||||
new String[]{"minecraft:overworld"}, "minecraft:overworld");
|
||||
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
|
||||
|
||||
// Prepare cached motions for the gravity check
|
||||
maxFallDistance = 0;
|
||||
@@ -130,9 +137,9 @@ public class FallbackPreparer {
|
||||
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
|
||||
spawnPosition = new FallbackPacketSnapshot(new PlayerPositionLookPacket(
|
||||
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0, -90, TELEPORT_ID, false));
|
||||
|
||||
@@ -163,9 +170,13 @@ public class FallbackPreparer {
|
||||
tooManyOnlinePerIP = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true));
|
||||
|
||||
// Prepare transfer packet
|
||||
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
|
||||
Sonar.get().getConfig().getVerification().getTransfer().getHost(),
|
||||
Sonar.get().getConfig().getVerification().getTransfer().getPort()));
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("verification.transfer.enabled")) {
|
||||
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("verification.transfer.destination-host"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getInt("verification.transfer.destination-port")));
|
||||
} else {
|
||||
transferToOrigin = null;
|
||||
}
|
||||
|
||||
// Prepare packets for the vehicle check
|
||||
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
|
||||
@@ -174,10 +185,14 @@ public class FallbackPreparer {
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
// Prepare CAPTCHA messages
|
||||
enterCodeMessage = new FallbackPacketSnapshot(new UniversalChatPacket(
|
||||
Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE));
|
||||
incorrectCaptcha = new FallbackPacketSnapshot(new UniversalChatPacket(
|
||||
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE));
|
||||
enterCodeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.enter-code"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
incorrectCaptcha = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.incorrect"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
|
||||
// Prepare countdown
|
||||
xpCountdown = new FallbackPacket[Sonar.get().getConfig().getVerification().getMap().getMaxDuration() / 1000];
|
||||
@@ -189,6 +204,11 @@ public class FallbackPreparer {
|
||||
|
||||
// Prepare CAPTCHA answers
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
} else {
|
||||
// Throw away if not needed
|
||||
enterCodeMessage = null;
|
||||
incorrectCaptcha = null;
|
||||
xpCountdown = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -79,6 +79,7 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10366;
|
||||
}
|
||||
}, 1D);
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ public enum ItemType {
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 982;
|
||||
}
|
||||
});
|
||||
|
||||
+2
@@ -39,6 +39,7 @@ public final class DimensionRegistry {
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_20;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_21;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
|
||||
static {
|
||||
@@ -48,6 +49,7 @@ public final class DimensionRegistry {
|
||||
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
|
||||
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
|
||||
CODEC_1_20 = getCodec("codec_1_20.nbt");
|
||||
CODEC_1_21 = getCodec("codec_1_21.nbt");
|
||||
OLD_CODEC = getCodec("codec_old.nbt");
|
||||
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
|
||||
|
||||
+1
@@ -78,6 +78,7 @@ public enum EntityType {
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10;
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ClientSettingsPacket implements FallbackPacket {
|
||||
public final class ClientInformationPacket implements FallbackPacket {
|
||||
private String locale;
|
||||
private byte viewDistance;
|
||||
private int chatVisibility;
|
||||
+1
-1
@@ -31,7 +31,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TeleportConfirmPacket implements FallbackPacket {
|
||||
public final class ConfirmTeleportationPacket implements FallbackPacket {
|
||||
private int teleportId;
|
||||
|
||||
@Override
|
||||
+2
-2
@@ -34,11 +34,11 @@ import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
@AllArgsConstructor
|
||||
public final class DisconnectPacket implements FallbackPacket {
|
||||
private @NotNull ComponentHolder componentHolder;
|
||||
private boolean login;
|
||||
private boolean duringLogin;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
componentHolder.write(byteBuf, login ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
|
||||
componentHolder.write(byteBuf, duringLogin ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ClientAbilitiesPacket implements FallbackPacket {
|
||||
public final class PlayerAbilitiesPacket implements FallbackPacket {
|
||||
private int encodedFlags;
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
@@ -42,7 +42,7 @@ public final class ClientAbilitiesPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -25,14 +26,16 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readRetainedByteBufSlice17;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readExtendedForgeShort;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class PluginMessagePacket implements FallbackPacket {
|
||||
private String channel;
|
||||
private ByteBuf slicedBuffer;
|
||||
private byte[] data;
|
||||
|
||||
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
@@ -41,13 +44,23 @@ public final class PluginMessagePacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
channel = readString(byteBuf, 64);
|
||||
channel = readString(byteBuf, 48);
|
||||
|
||||
final int length;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
slicedBuffer = byteBuf.readRetainedSlice(byteBuf.readableBytes());
|
||||
length = byteBuf.readableBytes();
|
||||
if (length > Short.MAX_VALUE) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
}
|
||||
} else {
|
||||
slicedBuffer = readRetainedByteBufSlice17(byteBuf);
|
||||
length = readExtendedForgeShort(byteBuf);
|
||||
if (length > FORGE_MAX_ARRAY_LENGTH) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
}
|
||||
}
|
||||
|
||||
data = new byte[length];
|
||||
byteBuf.readBytes(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetSlotPacket implements FallbackPacket {
|
||||
public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
private int slot, count, itemId;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class DefaultSpawnPositionPacket implements FallbackPacket {
|
||||
public final class SetDefaultSpawnPositionPacket implements FallbackPacket {
|
||||
private int x, y, z;
|
||||
|
||||
@Override
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetHeldItemPacket implements FallbackPacket {
|
||||
private int slot;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
slot = byteBuf.readShort();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerPositionIdlePacket implements FallbackPacket {
|
||||
public final class SetPlayerOnGround implements FallbackPacket {
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
+2
-2
@@ -32,7 +32,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerPositionPacket implements FallbackPacket {
|
||||
public final class SetPlayerPositionPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class PlayerPositionPacket implements FallbackPacket {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
onGround = byteBuf.readBoolean();
|
||||
+2
-2
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
public final class SetPlayerPositionRotation implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
private int teleportId;
|
||||
@@ -68,7 +68,7 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
+5
-2
@@ -26,8 +26,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -44,7 +47,7 @@ public final class SpawnEntityPacket implements FallbackPacket {
|
||||
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_8) > 0;
|
||||
|
||||
if (v1_9orHigher) {
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
writeUUID(byteBuf, UUID.randomUUID());
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
|
||||
+9
-22
@@ -22,37 +22,27 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class UniversalChatPacket implements FallbackPacket {
|
||||
public final class SystemChatPacket implements FallbackPacket {
|
||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
public static final byte SYSTEM_TYPE = (byte) 1;
|
||||
public static final byte GAME_INFO_TYPE = (byte) 2;
|
||||
|
||||
private ComponentHolder componentHolder;
|
||||
private String message;
|
||||
private byte type;
|
||||
private byte[] signature;
|
||||
|
||||
public UniversalChatPacket(final @NotNull Component component, final byte type) {
|
||||
this(new ComponentHolder(component), type);
|
||||
}
|
||||
|
||||
public UniversalChatPacket(final ComponentHolder componentHolder, final byte type) {
|
||||
public SystemChatPacket(final @NotNull ComponentHolder componentHolder) {
|
||||
this.componentHolder = componentHolder;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,17 +52,17 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
|
||||
// Type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(type == GAME_INFO_TYPE);
|
||||
byteBuf.writeBoolean(false); // it's not the GAME_INFO type
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, type);
|
||||
writeVarInt(byteBuf, 1); // system chat
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(type);
|
||||
byteBuf.writeByte(1); // system chat
|
||||
}
|
||||
|
||||
// Sender
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
writeUUID(byteBuf, UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +78,7 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
boolean unsigned = false;
|
||||
|
||||
if (saltLong != 0L && signatureBytes.length > 0) {
|
||||
signature = signatureBytes;
|
||||
// No need to store the valid signature
|
||||
} else if ((protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0
|
||||
|| saltLong == 0L) && signatureBytes.length == 0) {
|
||||
unsigned = true;
|
||||
@@ -125,9 +115,6 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
if (signed) {
|
||||
final byte[] sign = new byte[256];
|
||||
byteBuf.readBytes(sign);
|
||||
signature = sign;
|
||||
} else {
|
||||
signature = new byte[0];
|
||||
}
|
||||
|
||||
readVarInt(byteBuf);
|
||||
-2
@@ -55,8 +55,6 @@ public final class TransactionPacket implements FallbackPacket {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeShort((short) transactionId);
|
||||
// The "accepted" field is actually really unnecessary since
|
||||
// it's never even used in the client.
|
||||
byteBuf.writeBoolean(accepted);
|
||||
} else {
|
||||
byteBuf.writeInt(transactionId);
|
||||
|
||||
+9
-8
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SetSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* {@link SetContainerSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
|
||||
* <br>
|
||||
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
|
||||
@@ -49,9 +49,10 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
|
||||
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlotPacket(36, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// If the player is on Java, set the 5th slot (ID 4) in the player's hotbar to the map.
|
||||
// If the player is on Bedrock, set the 1st slot (ID 0) in the player's hotbar to the map.
|
||||
user.delayedWrite(new SetContainerSlotPacket(user.isGeyser() ? 36 : 40, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetContainerSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
final MapCaptchaInfo captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
this.answer = captcha.getAnswer();
|
||||
@@ -76,8 +77,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
checkState(!user.getLoginTimer().elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof UniversalChatPacket) {
|
||||
final UniversalChatPacket chat = (UniversalChatPacket) packet;
|
||||
if (packet instanceof SystemChatPacket) {
|
||||
final SystemChatPacket chat = (SystemChatPacket) packet;
|
||||
|
||||
// Captcha is correct, finish verification
|
||||
if (chat.getMessage().equals(answer)) {
|
||||
@@ -88,8 +89,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
|
||||
// Captcha is incorrect, remove one try
|
||||
checkState(tries-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
} else if (packet instanceof PlayerPositionPacket
|
||||
|| packet instanceof PlayerPositionLookPacket) {
|
||||
} else if (packet instanceof SetPlayerPositionPacket
|
||||
|| packet instanceof SetPlayerPositionRotation) {
|
||||
// A position packet is sent approximately every second
|
||||
final long difference = maxDuration - user.getLoginTimer().delay();
|
||||
final int index = (int) (difference / 1000D);
|
||||
|
||||
+32
-70
@@ -32,7 +32,9 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackGravitySessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackGravitySessionHandler(final FallbackUser user, final String username, final UUID uuid) {
|
||||
public FallbackGravitySessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
@@ -79,8 +81,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
|
||||
private final boolean enableGravityCheck, enableCollisionsCheck;
|
||||
private boolean teleported, checkMovement;
|
||||
private short expectedTransactionId = 1;
|
||||
private double x, y, z, deltaY;
|
||||
private double y, deltaY;
|
||||
private int movementTick;
|
||||
|
||||
private void markTeleported() {
|
||||
@@ -88,7 +89,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
teleported = true;
|
||||
}
|
||||
|
||||
private void markNextStage() {
|
||||
private void markSuccess(final boolean forceCAPTCHA) {
|
||||
// Force-stop the movement checks
|
||||
teleported = false;
|
||||
|
||||
@@ -98,77 +99,28 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
if (!user.isGeyser()) {
|
||||
checkState(user.isReceivedPluginMessage(), "didn't send plugin message");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses ping packets to check for an immediate, legitimate response from the client
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28configuration.29">Wiki.vg - Ping (configuration)</a>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28play.29">Wiki.vg - Ping (play)</a>
|
||||
*/
|
||||
private void sendTransaction() {
|
||||
// Interrupt any ongoing process/check
|
||||
markNextStage();
|
||||
|
||||
// Send a Transaction (Ping) packet with a random ID
|
||||
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
private void forceCAPTCHA() {
|
||||
// Interrupt any ongoing process/check
|
||||
markNextStage();
|
||||
|
||||
// Forcefully make the player solve a CAPTCHA
|
||||
// Send the player to the protocol check
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Either send the player to the vehicle check,
|
||||
// send the player to the CAPTCHA, or finish the verification.
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Pass the player to the next best verification handler
|
||||
if (Sonar.get().getFallback().shouldPerformVehicleCheck()) {
|
||||
decoder.setListener(new FallbackVehicleSessionHandler(user, username, uuid, x, y, z));
|
||||
} else if (Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
decoder.setListener(new FallbackProtocolSessionHandler(user, username, uuid, forceCAPTCHA));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
final long transactionId = transaction.getTransactionId();
|
||||
checkState(transactionId == expectedTransactionId,
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transactionId);
|
||||
|
||||
markSuccess();
|
||||
} else if (packet instanceof PlayerPositionLookPacket) {
|
||||
if (packet instanceof SetPlayerPositionRotation) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final PlayerPositionLookPacket positionLook = (PlayerPositionLookPacket) packet;
|
||||
final SetPlayerPositionRotation positionLook = (SetPlayerPositionRotation) packet;
|
||||
handleMovement(positionLook.getX(), positionLook.getY(), positionLook.getZ(), positionLook.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof PlayerPositionPacket) {
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof TeleportConfirmPacket) {
|
||||
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
|
||||
} else if (packet instanceof ConfirmTeleportationPacket) {
|
||||
final ConfirmTeleportationPacket teleportConfirm = (ConfirmTeleportationPacket) packet;
|
||||
|
||||
// Only expect this packet to be sent once
|
||||
checkState(!teleported, "duplicate teleport confirm");
|
||||
@@ -180,9 +132,9 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
markTeleported();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the play phase.
|
||||
else if (packet instanceof ClientSettingsPacket) {
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientSettings((ClientSettingsPacket) packet);
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
@@ -195,7 +147,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
// No need to continue checking if the gravity and collision checks are disabled
|
||||
// Continue with the next stage of the verification
|
||||
if (!enableGravityCheck && !enableCollisionsCheck) {
|
||||
sendTransaction();
|
||||
markSuccess(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,9 +165,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
final double lastDeltaY = this.deltaY;
|
||||
final double lastY = this.y;
|
||||
this.deltaY = y - lastY;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
@@ -237,6 +187,9 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// The movement tick should not be ignored when gravity check is disabled.
|
||||
movementTick++;
|
||||
|
||||
if (enableGravityCheck) {
|
||||
// Predict the player's current motion based on the last motion
|
||||
// https://minecraft.wiki/w/Entity#Motion_of_entities
|
||||
@@ -248,18 +201,27 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
if (difference > 1e-7) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
forceCAPTCHA();
|
||||
markSuccess(true);
|
||||
return;
|
||||
}
|
||||
user.fail("incorrect gravity; predicted: " + predicted + " deltaY: " + deltaY + " y: " + y);
|
||||
}
|
||||
|
||||
// The player is obeying gravity, go on to the next stage if the collision check is disabled.
|
||||
if (++movementTick == maxMovementTick && !enableCollisionsCheck) {
|
||||
sendTransaction();
|
||||
if (movementTick == maxMovementTick && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
}
|
||||
}
|
||||
} else if (enableCollisionsCheck) {
|
||||
// Make sure the player has actually moved before reaching the platform
|
||||
if (movementTick < maxMovementTick) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision tick: " + movementTick);
|
||||
}
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
// 1.7 sends the head position instead of the AABB minY
|
||||
@@ -270,7 +232,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkState(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player has collided with the blocks, go on to the next stage
|
||||
sendTransaction();
|
||||
markSuccess(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientSettingsPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.KeepAlivePacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
|
||||
@@ -60,7 +60,9 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
*/
|
||||
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackLoginSessionHandler(final FallbackUser user, final String username, final UUID uuid) {
|
||||
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
@@ -138,9 +140,9 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
markSuccess();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the configuration phase.
|
||||
else if (packet instanceof ClientSettingsPacket) {
|
||||
else if (packet instanceof ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientSettings((ClientSettingsPacket) packet);
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
@@ -159,7 +161,8 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
// 1.20.5+ adds new "game bundle features" which overcomplicate all of this...
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Write the new RegistrySync packets to the buffer
|
||||
for (final FallbackPacket syncPacket : REGISTRY_SYNC_1_20_5) {
|
||||
for (final FallbackPacket syncPacket : user.getProtocolVersion().compareTo(MINECRAFT_1_21) < 0
|
||||
? REGISTRY_SYNC_1_20_5 : REGISTRY_SYNC_1_21) {
|
||||
user.delayedWrite(syncPacket);
|
||||
}
|
||||
} else {
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetHeldItemPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Immediately send the player the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private short expectedTransactionId;
|
||||
private int currentClientSlotId, expectedSlotId = -1;
|
||||
|
||||
/**
|
||||
* Uses Transaction (ping) packets to check for an immediate, legitimate response from the client
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28configuration.29">Wiki.vg - Ping (configuration)</a>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28play.29">Wiki.vg - Ping (play)</a>
|
||||
*/
|
||||
private void sendTransaction() {
|
||||
// Send a Transaction (Ping) packet with a random ID
|
||||
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses SetHeldItem packets to check for a legitimate response from the client
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Set_Held_Item_.28serverbound.29">Wiki.vg - SetHeldItem (play)</a>
|
||||
*/
|
||||
private void sendSetHeldItem() {
|
||||
// Move the player's slot by 4 (slot limit divided by 2),
|
||||
// and then modulo it by the slot limit (8) to ensure that we don't send invalid slot IDs.
|
||||
expectedSlotId = (currentClientSlotId + 4) % 8;
|
||||
// Send two SetHeldItem packets with the same slot to check if the player responds with the correct slot.
|
||||
// By vanilla protocol, the client does not respond to duplicate SetHeldItem packets.
|
||||
// We can take advantage of this by sending two packets with the same content to check for a valid response.
|
||||
final SetHeldItemPacket heldItemPacket = new SetHeldItemPacket(expectedSlotId);
|
||||
user.delayedWrite(heldItemPacket);
|
||||
user.delayedWrite(heldItemPacket);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Either send the player to the vehicle check,
|
||||
// send the player to the CAPTCHA, or finish the verification.
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Pass the player to the next best verification handler
|
||||
if (Sonar.get().getFallback().shouldPerformVehicleCheck()) {
|
||||
decoder.setListener(new FallbackVehicleSessionHandler(user, username, uuid, forceCAPTCHA));
|
||||
} else if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
final long transactionId = transaction.getTransactionId();
|
||||
checkState(transactionId == expectedTransactionId,
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transactionId);
|
||||
|
||||
// Only perform the SetHeldItem check on Java players since the Bedrock protocol breaks this check.
|
||||
// I don't really know why this doesn't work on Bedrock,
|
||||
// but I think it has to do with how Geyser/floodgate translates this packet:
|
||||
// https://wiki.vg/Bedrock_Protocol#Player_Hotbar
|
||||
if (user.isGeyser()) {
|
||||
markSuccess();
|
||||
} else {
|
||||
sendSetHeldItem();
|
||||
}
|
||||
} else if (packet instanceof SetHeldItemPacket) {
|
||||
final SetHeldItemPacket heldItemPacket = (SetHeldItemPacket) packet;
|
||||
|
||||
final int slotId = heldItemPacket.getSlot();
|
||||
// Also check if the player sent an invalid slot which is impossible by vanilla protocol
|
||||
checkState(slotId >= 0 && slotId <= 8, "slot out of range: " + slotId);
|
||||
// Check if the player sent a duplicate slot packet which is impossible by vanilla protocol
|
||||
checkState(slotId != currentClientSlotId, "invalid slot: " + slotId);
|
||||
|
||||
// Only continue checking if we're actually expecting a SetHeldItem packet
|
||||
// The player can send a SetHeldItem packet by themselves -> exempt
|
||||
if (expectedSlotId != -1
|
||||
// Check if the slot ID matches the expected slot ID
|
||||
// This can false flag if a player spams these packets, which is why we don't fail for this
|
||||
&& slotId == expectedSlotId) {
|
||||
markSuccess();
|
||||
}
|
||||
|
||||
currentClientSlotId = slotId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-26
@@ -17,7 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.session;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -25,17 +24,17 @@ import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientSettingsPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_5;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
@@ -61,30 +60,31 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
final long timeTaken = user.getLoginTimer().delay();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(user.getInetAddress(), uuid, timeTaken));
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getInetAddress(), uuid, user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, uuid, user, timeTaken));
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
|
||||
username, uuid, user, user.getLoginTimer().delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (Sonar.get().getConfig().getVerification().getTransfer().isEnabled()
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
user.write(transferToOrigin);
|
||||
// Close the channel to ensure that the connection is closed
|
||||
user.getChannel().close();
|
||||
// Send the transfer packet to the player and close the channel
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess(), false);
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", user.getLoginTimer().toString()));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", username)
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void checkClientSettings(final @NotNull ClientSettingsPacket clientSettings) {
|
||||
protected final void checkClientInformation(final @NotNull ClientInformationPacket clientSettings) {
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientSettings.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
@@ -113,25 +113,30 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getSlicedBuffer());
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
// Mark the PluginMessage packet as received
|
||||
user.setReceivedPluginMessage(true);
|
||||
}
|
||||
|
||||
protected final void validateClientBrand(final @NotNull ByteBuf content) {
|
||||
// No need to check for empty brands since ProtocolUtil#readBrandMessage
|
||||
// already performs invalid string checks by default.
|
||||
final String read = ProtocolUtil.readBrandMessage(content);
|
||||
protected final void validateClientBrand(final byte @NotNull [] data) {
|
||||
// Check if the client brand is too short. It has to have at least 2 bytes.
|
||||
checkState(data.length > 1, "client brand is too short");
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(read.length() < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand is too long: " + read.length());
|
||||
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand contains too much data: " + data.length);
|
||||
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
|
||||
String brand = new String(data, StandardCharsets.UTF_8);
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
brand = brand.substring(1);
|
||||
}
|
||||
// Check for illegal client brands
|
||||
checkState(!read.equals("Vanilla"), "illegal client brand: " + read);
|
||||
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(read).matches(), "client brand does not match pattern");
|
||||
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern");
|
||||
}
|
||||
|
||||
protected final void validateClientLocale(final @NotNull String locale) {
|
||||
|
||||
+7
-3
@@ -51,21 +51,25 @@ public final class FallbackVehicleSessionHandler extends FallbackSessionHandler
|
||||
public FallbackVehicleSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid,
|
||||
final double x, final double y, final double z) {
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Send the necessary packets to mount the player on the vehicle
|
||||
final int entityTypeId = EntityType.BOAT.getId(user.getProtocolVersion());
|
||||
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, entityTypeId, x, y, z));
|
||||
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, entityTypeId,
|
||||
SPAWN_X_POSITION, DEFAULT_Y_COLLIDE_POSITION, SPAWN_Z_POSITION));
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private boolean receivedPaddle, receivedInput;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Make sure the player exits the vehicle before sending the CAPTCHA
|
||||
user.delayedWrite(removeEntities);
|
||||
// Either send the player to the CAPTCHA, or finish the verification.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@UtilityClass
|
||||
public final class SonarServiceManager {
|
||||
private final ScheduledExecutorService VERBOSE = createScheduledExecutor("sonar-verbose-thread");
|
||||
private final ScheduledExecutorService FALLBACK_QUEUE = createScheduledExecutor("sonar-queue-thread");
|
||||
private final ScheduledExecutorService STATISTICS = createScheduledExecutor("sonar-statistics-thread");
|
||||
|
||||
private @NotNull ScheduledExecutorService createScheduledExecutor(final String threadName) {
|
||||
return Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
final Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setName(threadName);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
public void start() {
|
||||
VERBOSE.scheduleAtFixedRate(() -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().observe();
|
||||
}, 0L, 250L, TimeUnit.MILLISECONDS);
|
||||
|
||||
FALLBACK_QUEUE.scheduleAtFixedRate(() -> Sonar.get().getFallback().getQueue().poll(),
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
STATISTICS.scheduleAtFixedRate(CachedBandwidthStatistics::reset,
|
||||
0L, 1L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
VERBOSE.shutdown();
|
||||
FALLBACK_QUEUE.shutdown();
|
||||
STATISTICS.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
public final class SonarServiceThread extends Thread {
|
||||
private final long delay;
|
||||
private final Runnable runnable;
|
||||
|
||||
public SonarServiceThread(final String name, final long delay, final Runnable runnable) {
|
||||
setName(name);
|
||||
this.delay = delay;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
runnable.run();
|
||||
// Sleep thread by specified delay
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
} catch (InterruptedException interruptedException) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
@UtilityClass
|
||||
public final class SonarServiceThreadManager {
|
||||
private static final SonarServiceThread VERBOSE = new SonarServiceThread("sonar-verbose-thread",
|
||||
250L, () -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().update();
|
||||
});
|
||||
private static final SonarServiceThread FALLBACK_QUEUE = new SonarServiceThread("sonar-fallback-queue-thread",
|
||||
1000L, () -> Sonar.get().getFallback().getQueue().poll());
|
||||
private static final SonarServiceThread STATISTICS = new SonarServiceThread("sonar-statistics-thread",
|
||||
1000L, CachedBandwidthStatistics::reset);
|
||||
|
||||
public void start() {
|
||||
VERBOSE.start();
|
||||
FALLBACK_QUEUE.start();
|
||||
STATISTICS.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
VERBOSE.interrupt();
|
||||
FALLBACK_QUEUE.interrupt();
|
||||
STATISTICS.interrupt();
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "blacklist",
|
||||
description = "Manage blacklisted IP addresses",
|
||||
arguments = {
|
||||
@Argument("add"),
|
||||
@Argument("remove"),
|
||||
@Argument("clear"),
|
||||
@Argument("size")
|
||||
}
|
||||
)
|
||||
public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "blacklist add <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
SONAR.getFallback().getBlacklist().put(inetAddress, (byte) 0);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "blacklist remove <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
SONAR.getFallback().getBlacklist().invalidate(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final long blacklisted = SONAR.getFallback().getBlacklist().estimatedSize();
|
||||
|
||||
if (blacklisted == 0) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
SONAR.getFallback().getBlacklist().invalidateAll();
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize())));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
incorrectUsage(invocation.getSender());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "statistics",
|
||||
aliases = {"stats"},
|
||||
description = "Show session statistics of this server",
|
||||
arguments = {
|
||||
@Argument("network"),
|
||||
@Argument("memory"),
|
||||
@Argument("cpu")
|
||||
},
|
||||
argumentsRequired = false
|
||||
)
|
||||
public final class StatisticsCommand extends Subcommand {
|
||||
private enum StatisticType {
|
||||
GENERAL,
|
||||
NETWORK,
|
||||
MEMORY,
|
||||
CPU
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
StatisticType type = StatisticType.GENERAL;
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
try {
|
||||
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
|
||||
} catch (Exception exception) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
|
||||
.replace("%statistics%", getArguments()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
|
||||
.replace("%type%", type.name().toLowerCase()));
|
||||
invocation.getSender().sendMessage(Component.empty());
|
||||
|
||||
switch (type) {
|
||||
case GENERAL: {
|
||||
final long seconds = Sonar.get().getLaunchTimer().delay() / 1000L;
|
||||
final long days = seconds / (24L * 60L * 60L);
|
||||
final long hours = (seconds % (24L * 60L * 60L)) / (60L * 60L);
|
||||
final long minutes = (seconds % (60L * 60L)) / 60L;
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
|
||||
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%uptime%", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L))
|
||||
.replace("%total_joins%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined()))
|
||||
.replace("%total_attempts%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications()))
|
||||
.replace("%total_failed%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())));
|
||||
break;
|
||||
}
|
||||
|
||||
case CPU: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
|
||||
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
|
||||
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
|
||||
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
|
||||
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
|
||||
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
|
||||
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
|
||||
break;
|
||||
}
|
||||
|
||||
case MEMORY: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
|
||||
.replace("%free_memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%used_memory%", formatMemory(getUsedMemory()))
|
||||
.replace("%max_memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%total_memory%", formatMemory(getTotalMemory())));
|
||||
break;
|
||||
}
|
||||
|
||||
case NETWORK: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
|
||||
.replace("%incoming%", CachedBandwidthStatistics.INCOMING.getCachedSecond())
|
||||
.replace("%outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedSecond())
|
||||
.replace("%ttl_incoming%", CachedBandwidthStatistics.INCOMING.getCachedTtl())
|
||||
.replace("%ttl_outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedTtl()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verified",
|
||||
description = "Manage verified IP addresses",
|
||||
arguments = {
|
||||
@Argument("history"),
|
||||
@Argument("remove"),
|
||||
@Argument("add"),
|
||||
@Argument("clear"),
|
||||
@Argument("size")
|
||||
}
|
||||
)
|
||||
public final class VerifiedCommand extends Subcommand {
|
||||
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified history <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is verified already
|
||||
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
|
||||
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified remove <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
|
||||
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
|
||||
if (LOCK.contains(rawInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the IP address
|
||||
// Make sure we don't accidentally run 2 operations at the same time
|
||||
LOCK.add(rawInetAddress);
|
||||
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
// Unlock the IP address
|
||||
LOCK.remove(rawInetAddress);
|
||||
break;
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified add <IP address> <UUID/username>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
|
||||
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
|
||||
if (LOCK.contains(rawInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Try to parse the UUID (from the username, if needed)
|
||||
final String rawUUID = invocation.getRawArguments()[3];
|
||||
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
|
||||
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString(), uuid)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAlready());
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the IP address
|
||||
// Make sure we don't accidentally run 2 operations at the same time
|
||||
LOCK.add(rawInetAddress);
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
// Unlock the IP address
|
||||
LOCK.remove(rawInetAddress);
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
|
||||
|
||||
if (verified == 0) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
SONAR.getVerifiedPlayerController().clearAll();
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
incorrectUsage(invocation.getSender());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "blacklist",
|
||||
arguments = {"add", "remove", "clear", "size"}
|
||||
)
|
||||
public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "blacklist add <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
try {
|
||||
// Try to resolve the IP address, so we can actually do something with it
|
||||
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
Sonar.get().getFallback().getBlacklist().put(inetAddress, (byte) 0);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSource().sendMessage(Component.text(
|
||||
"Unexpected error, check console.", NamedTextColor.RED));
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "blacklist remove <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
try {
|
||||
// Try to resolve the IP address, so we can actually do something with it
|
||||
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
Sonar.get().getFallback().getBlacklist().invalidate(inetAddress);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSource().sendMessage(Component.text(
|
||||
"Unexpected error, check console.", NamedTextColor.RED));
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final long blacklistSize = Sonar.get().getFallback().getBlacklist().estimatedSize();
|
||||
|
||||
if (blacklistSize == 0) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.empty"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getFallback().getBlacklist().invalidateAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.clear"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(blacklistSize))));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
final long blacklistSize = Sonar.get().getFallback().getBlacklist().estimatedSize();
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.size"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(blacklistSize))));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
incorrectUsage(invocation.getSource());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-15
@@ -15,13 +15,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
@@ -30,7 +30,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
@@ -39,7 +38,6 @@ import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
// the Sonar contributors to fix issues more quickly
|
||||
@SubcommandInfo(
|
||||
name = "dump",
|
||||
description = "Print developer information",
|
||||
onlyConsole = true
|
||||
)
|
||||
public final class DumpCommand extends Subcommand {
|
||||
@@ -49,17 +47,12 @@ public final class DumpCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = GSON.toJson(collectMappedInformation());
|
||||
SONAR.getLogger().info("Generated dump: {}", json);
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
private @NotNull Map<String, Object> collectMappedInformation() {
|
||||
final var mappings = new WeakHashMap<String, Object>();
|
||||
mappings.put("sonar", new Dump.Sonar(
|
||||
SONAR.getVersion().getFull(),
|
||||
SONAR.getPlatform(),
|
||||
SONAR.getVersion().isOnMainBranch()
|
||||
Sonar.get().getVersion().getFormatted(),
|
||||
Sonar.get().getPlatform(),
|
||||
Sonar.get().getVersion().getGitBranch(),
|
||||
Sonar.get().getVersion().getGitCommit()
|
||||
));
|
||||
mappings.put("runtime", new Dump.Runtime(
|
||||
getVirtualCores(),
|
||||
@@ -79,7 +72,8 @@ public final class DumpCommand extends Subcommand {
|
||||
formatMemory(getFreeMemory()),
|
||||
formatMemory(getUsedMemory())
|
||||
));
|
||||
return mappings;
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getMessagesConfig().getString("commands.dump.log")
|
||||
.replace("<dumped-json-data>", GSON.toJson(mappings)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -89,7 +83,8 @@ public final class DumpCommand extends Subcommand {
|
||||
private static final class Sonar {
|
||||
private final String version;
|
||||
private final SonarPlatform platform;
|
||||
private final boolean isOnMainBranch;
|
||||
private final String gitBranch;
|
||||
private final String gitCommit;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
+13
-7
@@ -15,29 +15,35 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "notify",
|
||||
description = "Enable or disable attack notifications",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class NotifyCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getUuid())) {
|
||||
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getUuid());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsUnsubscribed());
|
||||
if (Sonar.get().getNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getNotificationHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.unsubscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsSubscribed());
|
||||
SONAR.getNotificationHandler().subscribe(invocation.getSender().getUuid());
|
||||
Sonar.get().getNotificationHandler().subscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.subscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -15,28 +15,35 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "reload",
|
||||
description = "Reload all configurations"
|
||||
name = "reload"
|
||||
)
|
||||
public final class ReloadCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.reload.start"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
|
||||
SONAR.reload();
|
||||
Sonar.get().reload();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
|
||||
.replace("%taken%", String.valueOf(timer.delay())));
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.reload.finish"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("time-taken", String.valueOf(timer.delay()))));
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
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.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "statistics",
|
||||
aliases = {"stats"},
|
||||
arguments = {"network", "memory", "cpu"},
|
||||
argumentsRequired = false
|
||||
)
|
||||
public final class StatisticsCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
String type = "general";
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
try {
|
||||
type = invocation.getRawArguments()[1].toLowerCase();
|
||||
} catch (Exception exception) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.statistics.unknown-type"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("statistics", getArguments())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.statistics.header"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("statistics-type", type)));
|
||||
invocation.getSource().sendMessage(Component.empty());
|
||||
|
||||
TagResolver.@NotNull Single[] placeholders = null;
|
||||
|
||||
switch (type) {
|
||||
case "general": {
|
||||
final long seconds = Sonar.get().getLaunchTimer().delay() / 1000L;
|
||||
final long days = seconds / (24L * 60L * 60L);
|
||||
final long hours = (seconds % (24L * 60L * 60L)) / (60L * 60L);
|
||||
final long minutes = (seconds % (60L * 60L)) / 60L;
|
||||
|
||||
placeholders = new TagResolver.Single[]{
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("verified", DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize())),
|
||||
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
|
||||
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
|
||||
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
|
||||
Placeholder.unparsed("server-uptime", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L)),
|
||||
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
|
||||
Placeholder.unparsed("total-attempts", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("total-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications()))
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case "cpu": {
|
||||
placeholders = new TagResolver.Single[]{
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("process-cpu", DECIMAL_FORMAT.format(getProcessCPUUsage())),
|
||||
Placeholder.unparsed("system-cpu", DECIMAL_FORMAT.format(getSystemCPUUsage())),
|
||||
Placeholder.unparsed("average-process-cpu", DECIMAL_FORMAT.format(getAverageProcessCPUUsage())),
|
||||
Placeholder.unparsed("average-system-cpu", DECIMAL_FORMAT.format(getAverageSystemCPUUsage())),
|
||||
Placeholder.unparsed("virtual-core-count", DECIMAL_FORMAT.format(getVirtualCores()))
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case "memory": {
|
||||
placeholders = new TagResolver.Single[]{
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory()))
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case "network": {
|
||||
placeholders = new TagResolver.Single[]{
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("incoming-traffic", CachedBandwidthStatistics.INCOMING.getCachedSecond()),
|
||||
Placeholder.unparsed("outgoing-traffic", CachedBandwidthStatistics.OUTGOING.getCachedSecond()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", CachedBandwidthStatistics.INCOMING.getCachedTtl()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", CachedBandwidthStatistics.OUTGOING.getCachedTtl())
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (final String msg : Sonar.get().getConfig().getMessagesConfig().getStringList("commands.statistics." + type)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(msg, placeholders));
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-8
@@ -15,32 +15,38 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verbose",
|
||||
description = "Enable or disable Sonar verbose",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class VerboseCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getUuid())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getUuid());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
if (Sonar.get().getVerboseHandler().isSubscribed(invocation.getSource().getUuid())) {
|
||||
Sonar.get().getVerboseHandler().unsubscribe(invocation.getSource().getUuid());
|
||||
// Reset ActionBar component when unsubscribing
|
||||
invocation.getSender().getAudience().sendActionBar(Component.empty());
|
||||
invocation.getSource().getAudience().sendActionBar(Component.empty());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.unsubscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(invocation.getSender().getUuid());
|
||||
Sonar.get().getVerboseHandler().subscribe(invocation.getSource().getUuid());
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.subscribe"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommands;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verified",
|
||||
arguments = {"add", "remove", "clear", "size", "history"}
|
||||
)
|
||||
public final class VerifiedCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified history <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
|
||||
// Make sure the IP is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
|
||||
for (final UUID uuid : Sonar.get().getVerifiedPlayerController().getUUIDs(rawAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history-entry"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("uuid", uuid.toString())));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
incorrectUsage(invocation.getSource(), "verified remove <IP address>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
// An InetAddress starts with /, we need to account for this
|
||||
final String inetAddress = "/" + rawAddress;
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (!Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
Sonar.get().getVerifiedPlayerController().remove(inetAddress);
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
incorrectUsage(invocation.getSource(), "verified add <IP address> <UUID/username>");
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
|
||||
// Make sure the given IP address is valid
|
||||
if (rawAddress == null) return;
|
||||
// An InetAddress starts with /, we need to account for this
|
||||
final String inetAddress = "/" + rawAddress;
|
||||
|
||||
// Try to parse the UUID (from the username, if needed)
|
||||
final String rawUUID = invocation.getRawArguments()[3];
|
||||
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
|
||||
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("ip", rawAddress)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
|
||||
if (verifiedSize == 0) {
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.empty"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
Sonar.get().getVerifiedPlayerController().clearAll();
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.clear"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(verifiedSize))));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
|
||||
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.size"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(verifiedSize))));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
incorrectUsage(invocation.getSource());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,56 @@ public class UpdateChecker {
|
||||
}
|
||||
};
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
|
||||
final HttpsURLConnection urlConnection = prepareConnection(url);
|
||||
final JsonObject json = parseJson(urlConnection);
|
||||
final String latestStableRelease = json.get("tag_name").getAsString();
|
||||
final int convertedLatestVersion = convertVersion(latestStableRelease);
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getVersion());
|
||||
|
||||
if (convertedCurrentVersion < convertedLatestVersion) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
|
||||
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().getGitBranch().equals("main")) {
|
||||
LOGGER.warn("You are currently using an unreleased version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
} else {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.warn("Unable to retrieve version information: {}", throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
|
||||
try (final InputStream inputStream = urlConnection.getInputStream();
|
||||
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
|
||||
final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(bufferedReader).getAsJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
urlConnection.setReadTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
|
||||
// Only continue if the connection is a success
|
||||
if (urlConnection.getResponseCode() != 200) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
return urlConnection;
|
||||
}
|
||||
|
||||
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
|
||||
final String[] convertedParts = version.split("\\.");
|
||||
// Validate the length of the converted parts
|
||||
@@ -68,66 +118,4 @@ public class UpdateChecker {
|
||||
final int patch = Integer.parseInt(convertedParts[2]);
|
||||
return convertedMajor + convertedMinor + patch; // sum the converted version parts
|
||||
}
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
|
||||
final HttpsURLConnection urlConnection = prepareConnection(url);
|
||||
final JsonObject json = parseJson(urlConnection);
|
||||
final String latestStableRelease = json.get("tag_name").getAsString();
|
||||
final int convertedLatestVersion = convertVersion(latestStableRelease);
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
|
||||
|
||||
if (convertedCurrentVersion < convertedLatestVersion) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
|
||||
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
|
||||
LOGGER.warn("You are currently using an unreleased version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
} else {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Could not retrieve latest version information: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
|
||||
try (final InputStream inputStream = urlConnection.getInputStream()) {
|
||||
try (final InputStreamReader inputStreamReader = new InputStreamReader(inputStream)) {
|
||||
final StringBuilder content = new StringBuilder();
|
||||
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
urlConnection.setReadTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestMethod("GET");
|
||||
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
|
||||
// Only continue if the connection is a success
|
||||
if (urlConnection.getResponseCode() != 200) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
return urlConnection;
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_3;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeString;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
|
||||
public final class ComponentHolder {
|
||||
private final String serializedComponent;
|
||||
private BinaryTag cachedBinaryTag;
|
||||
@@ -44,6 +43,8 @@ public final class ComponentHolder {
|
||||
this.serializedComponent = GsonComponentSerializer.gson().serialize(component);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
|
||||
private BinaryTag serialize(final JsonElement json) {
|
||||
if (json instanceof JsonPrimitive) {
|
||||
final JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
|
||||
@@ -143,7 +144,7 @@ public final class ComponentHolder {
|
||||
|
||||
public void write(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
if (cachedBinaryTag == null) {
|
||||
cachedBinaryTag = serialize(new JsonParser().parse(serializedComponent));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
@@ -39,10 +38,6 @@ import java.util.UUID;
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
public class ProtocolUtil {
|
||||
public static final UUID EMPTY_UUID = new UUID(0L, 0L);
|
||||
|
||||
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
|
||||
|
||||
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
|
||||
public static final String BRAND_CHANNEL = "minecraft:brand";
|
||||
|
||||
@@ -184,15 +179,6 @@ public class ProtocolUtil {
|
||||
return array;
|
||||
}
|
||||
|
||||
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
|
||||
final ByteBuf slice = content.slice();
|
||||
try {
|
||||
return readString(slice);
|
||||
} catch (DecoderException exception) {
|
||||
return readString(slice, 65536, slice.readableBytes());
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
|
||||
return readString(byteBuf, Short.MAX_VALUE);
|
||||
}
|
||||
@@ -210,7 +196,7 @@ public class ProtocolUtil {
|
||||
checkFrame(length <= cap * 3, "Bad string size");
|
||||
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
|
||||
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
|
||||
byteBuf.skipBytes(length);
|
||||
byteBuf.readerIndex(byteBuf.readerIndex() + length);
|
||||
checkFrame(str.length() <= cap, "Got a too-long string");
|
||||
return str;
|
||||
}
|
||||
@@ -315,7 +301,7 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static int readExtendedForgeShort(final @NotNull ByteBuf byteBuf) {
|
||||
public static int readExtendedForgeShort(final @NotNull ByteBuf byteBuf) {
|
||||
int low = byteBuf.readUnsignedShort();
|
||||
int high = 0;
|
||||
if ((low & 0x8000) != 0) {
|
||||
@@ -325,12 +311,6 @@ public class ProtocolUtil {
|
||||
return ((high & 0xFF) << 15) | low;
|
||||
}
|
||||
|
||||
public static ByteBuf readRetainedByteBufSlice17(final ByteBuf byteBuf) {
|
||||
final int length = readExtendedForgeShort(byteBuf);
|
||||
checkFrame(length <= FORGE_MAX_ARRAY_LENGTH, "Too long");
|
||||
return byteBuf.readRetainedSlice(length);
|
||||
}
|
||||
|
||||
private void checkFrame(final boolean expression, final String message) {
|
||||
if (!expression) {
|
||||
throw new CorruptedFrameException(message);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,9 @@
|
||||
|
||||
# Obecné nastavení pro Sonar
|
||||
general:
|
||||
# Měl by Sonar automaticky kontrolovat nové aktualizace?
|
||||
# Sonar používá GitHub API ke kontrole, zda je aktuální verze zastaralá.
|
||||
check-for-updates: true
|
||||
# Měl by Sonar vypisovat IP adresy hráču do konzole?
|
||||
log-player-addresses: true
|
||||
# Maximum hráču online se stejnou IP adresou
|
||||
@@ -40,8 +43,10 @@ attack-tracker:
|
||||
# Nicméně, IP adresy nemůžou být vystopovány zpátky k hráči, protože Sonar používá UUID na ukládní místo jmeno hráčů
|
||||
database:
|
||||
# Typy datábázi se kterýma Sonar bude pracovat
|
||||
# Typy: NONE, MYSQL, MARIADB
|
||||
# Typy: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Soubor v adresáři zásuvných modulů Sonaru použitý pro databázi H2
|
||||
filename: verified_players
|
||||
# Host adresa pro verifikování hráčů s SQL databází
|
||||
host: localhost
|
||||
# Port pro verifikaci s databází
|
||||
@@ -170,21 +175,21 @@ verification:
|
||||
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
|
||||
max-login-packets: 256
|
||||
|
||||
# Je-li povoleno, přehrávač bude přenesen zpět na původní server
|
||||
# po úspěšném absolvování ověření robota.
|
||||
# Je-li povoleno, hráč bude přenesen zpět na původní server
|
||||
# po úspěšném absolvování ověření bota.
|
||||
# Tuto funkci představil Mojang ve verzi Minecraft 1.20.5
|
||||
transfer:
|
||||
# Měl by Sonar přenést hráče na původní server (místo toho, aby je kopl)?
|
||||
# Měl by Sonar přenést hráče na původní server (místo toho, aby je vyhodil)?
|
||||
# Aby to fungovalo, musíte tuto funkci povolit v konfiguraci vašeho serveru
|
||||
# Kromě toho možná budete chtít snížit množství omezení rychlosti přihlášení
|
||||
# provádí Velocity nebo jiné proxy/pluginy, protože to může zabránit
|
||||
# hráč nebyl převeden správně.
|
||||
# hráči správné přenesení.
|
||||
enabled: false
|
||||
# Na který server by měl Sonar přenést přehrávač, když projde ověřením?
|
||||
# Na který server by měl Sonar přenést hráče, když projde ověřením?
|
||||
# Zadejte IP serveru, který hráči používají k běžnému připojení k vašemu serveru.
|
||||
# Například můžete zadat "mc.hypixel.net" nebo přímou IP jako "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Jaký port by měl Sonar použít při přenosu přehrávače na původní server?
|
||||
# Jaký port by měl Sonar použít při přenosu hráče na původní server?
|
||||
# Pokud váš server nepotřebuje port pro připojení, můžete toto ponechat jako 25565.
|
||||
destination-port: 25565
|
||||
|
||||
@@ -199,9 +204,12 @@ verification:
|
||||
# Tato funkce je experimentální a může způsobit problémy
|
||||
# Pokud je toto zakázáno, hráči Bedrock budou přeskočeni
|
||||
check-geyser-players: true
|
||||
# Množství času který musí uběhnout aby hráč byl odpojen
|
||||
# Doba, za kterou hráč přestal odesílat pakety před odpojením
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
read-timeout: 8000
|
||||
# Doba od okamžiku, kdy server přestal posílat pakety před odpojením hráče
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
write-timeout: 10000
|
||||
# Jak dlouho by měl hráč čekat, než se při ověřování znovu připojí
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -15,30 +15,35 @@
|
||||
|
||||
# Generelle Einstellungen für Sonar
|
||||
general:
|
||||
# Should Sonar automatically check for new updates?
|
||||
# Sonar uses the GitHub API to check if the current version is outdated
|
||||
check-for-updates: true
|
||||
# Soll Sonar die IP Adressen von Spielern in der Konsole anzeigen?
|
||||
log-player-addresses: true
|
||||
# Maximale Anzahl an Spielern mit der selben IP Adresse
|
||||
# Maximale Anzahl an Spielern mit derselben IP Adresse
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Sensitivitätseinstellungen für Angriffstracker
|
||||
# Bearbeite diese Einstellungen nur wenn du weißt was du tust!
|
||||
# Bearbeite diese Einstellungen, nur wenn du weißt, was du tust!
|
||||
attack-tracker:
|
||||
# Minimale Anzahl an neuen Spielern damit ein Angriff als solcher erkannt wird
|
||||
min-players-for-attack: 8
|
||||
# Anzahl an Millisekunden bis ein Angriff vorbei ist
|
||||
min-attack-duration: 30000
|
||||
# Anzahl an gemeldenten Vorfällen damit ein Angriff als solcher erkannt wird
|
||||
# Diese Zahl repräsentiert einen Puffer um falsche Angriffsbenachrichtigungen zu filtern
|
||||
# Diese Zahl repräsentiert einen Puffer, um falsche Angriffsbenachrichtigungen zu filtern
|
||||
min-attack-threshold: 2
|
||||
# Anzahl an Millisekunden die vergehen muss damit eine neue Attacke erkannt wird
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# Du kannst Sonar mit einer Datenbank verbinden um verifizierte Spieler zu speichern
|
||||
# Du kannst Sonar mit einer Datenbank verbinden, um verifizierte Spieler zu speichern
|
||||
# Beachte: IP adressen werden im Klartext gespeichert. Du bist für die Sicherheit der Datenbank verantwortlich
|
||||
database:
|
||||
# Typ von Datenbank die Sonar verwenden soll um verifizierte Spielerdaten zu speichern
|
||||
# Mögliche Typen: NONE, MYSQL, MARIADB
|
||||
# Typ von Datenbank die Sonar verwenden soll, um verifizierte Spielerdaten zu speichern
|
||||
# Mögliche Typen: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Datei im Plugin-Verzeichnis von Sonar, die für die H2-Datenbank verwendet wird
|
||||
filename: verified_players
|
||||
# HOST für die Authentifizierung mit der SQL Datenbank
|
||||
host: localhost
|
||||
# Port für die Authentifizierung mit der SQL Datenbank
|
||||
@@ -196,9 +201,12 @@ verification:
|
||||
# Diese Funktion ist experimentell und kann Probleme verursachen
|
||||
# Wenn dies deaktiviert ist, werden Bedrock-Spieler übersprungen
|
||||
check-geyser-players: true
|
||||
# Zeit, die verstreichen muss, bevor ein Spieler ein Timeout erleidet
|
||||
# Die Zeit, seit der der Spieler keine Pakete mehr sendet, bevor er die Verbindung trennt
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
read-timeout: 8000
|
||||
# Die Zeit, seit der der Server keine Pakete mehr sendet, bevor er die Verbindung zum Spieler trennt
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
write-timeout: 10000
|
||||
# Wie lange sollte ein Spieler warten, bevor er während der Überprüfung erneut verbinden kann
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
# General settings for Sonar
|
||||
general:
|
||||
# Should Sonar automatically check for new updates?
|
||||
# Sonar uses the GitHub API to check if the current version is outdated
|
||||
check-for-updates: true
|
||||
# Should Sonar log players' IP addresses in console?
|
||||
log-player-addresses: true
|
||||
# Maximum number of players online with the same IP address
|
||||
@@ -39,8 +42,10 @@ attack-tracker:
|
||||
# Note: IP addresses are saved in plain text. You are responsible for keeping your database safe!
|
||||
database:
|
||||
# Type of database Sonar uses to store verified players
|
||||
# Possible types: NONE, MYSQL, MARIADB
|
||||
# Possible types: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# File in Sonar's plugin directory used for the H2 database
|
||||
filename: verified_players
|
||||
# Host address for authenticating with the SQL database
|
||||
host: localhost
|
||||
# Port for authenticating with the SQL database
|
||||
@@ -198,13 +203,16 @@ verification:
|
||||
# This feature is experimental and might cause issues
|
||||
# If this is disabled, Bedrock players will be skipped
|
||||
check-geyser-players: true
|
||||
# Amount of time that has to pass before a player times out
|
||||
# The time since the player stopped sending packets before disconnecting
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
read-timeout: 8000
|
||||
# The time since the server stopped sending packets before disconnecting the player
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
write-timeout: 10000
|
||||
# How long should a player wait before reconnecting during verification
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# How long should Sonar remember the number of failed verifications for a player?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
remember-time: 120000
|
||||
# How long should an IP address be denied from logging in when failing the verification too often?
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
# Paramètre général de Sonar
|
||||
general:
|
||||
# Devrait Sonar vérifier automatiquement les nouvelles mises à jour ?
|
||||
# Sonar utilise l'API GitHub pour vérifier si la version actuelle est obsolète.
|
||||
check-for-updates: true
|
||||
# Devrait Sonar enregistrer les adresses IP des joueurs dans la console ?
|
||||
log-player-addresses: true
|
||||
# Nombre maximum de joueurs en ligne avec la même IP
|
||||
@@ -39,8 +42,10 @@ attack-tracker:
|
||||
# Remarque : Les adresses IP sont enregistrées en texte brut. Vous êtes responsable de la sécurité de votre base de données !
|
||||
database:
|
||||
# Type de base de données utilisé par Sonar pour stocker les joueurs vérifiés
|
||||
# Types possibles : NONE, MYSQL, MARIADB
|
||||
# Types possibles : NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Fichier dans le répertoire des plugins de Sonar utilisé pour la base de données H2
|
||||
filename: verified_players
|
||||
# Adresse de l'hôte pour l'authentification avec la base de données SQL
|
||||
host: localhost
|
||||
# Port pour l'authentification avec la base de données SQL
|
||||
@@ -198,9 +203,12 @@ verification:
|
||||
# Cette fonctionnalité est expérimentale et peut causer des problèmes
|
||||
# Si cela est désactivé, les joueurs Bedrock seront ignorés
|
||||
check-geyser-players: true
|
||||
# Durée avant qu'un joueur ne soit déconnecté
|
||||
# Temps écoulé depuis que le joueur a cessé d'envoyer des paquets avant de se déconnecter
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
read-timeout: 8000
|
||||
# Temps écoulé depuis que le serveur a cessé d'envoyer des paquets avant de se déconnecter
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
write-timeout: 10000
|
||||
# Combien de temps un joueur doit-il attendre avant de se reconnecter pendant la vérification
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
|
||||
# ზოგადი პარამეტრები Sonar-ისთვის
|
||||
general:
|
||||
# შეამოწმოს თუ არა სონარმა ავტომატურად ახალი განახლებები?
|
||||
# სონარი იყენებს GitHub API_ს რომ შეამოწმოს არის თუ არა ამჟამინდელი ვერსია ყველაზე ახალი.
|
||||
check-for-updates: true
|
||||
# უნდა ჩაწეროს თუ არა Sonar-მა მოთამაშეების IP მისამართები კონსოლში?
|
||||
log-player-addresses: true
|
||||
# მაქსიმალური ერთი და იგივე IP ზე ონლაინ მოთამაშეთა რაოდენობა
|
||||
# ერთი და იგივე IP ზე ონლაინ მოთამაშეთა მაქსიმალური რაოდენობა
|
||||
max-online-per-ip: 3
|
||||
|
||||
# მნიშვნელოვანი პარამეტრები თავდასხმის დაფიქსირებისთვის
|
||||
@@ -28,7 +31,7 @@ attack-tracker:
|
||||
# დრო, რის შემდეგაც შეტევა დასრულებული ჩაითვლება
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
min-attack-duration: 30000
|
||||
# რამდენჯერ უნდა მოხდეს ზედა პარამეტრები, რათა ჩაითვალოს ეს თავდასხმად
|
||||
# რამდენჯერ უნდა მოხდეს ინციდენტის დარეპორტება, რათა ჩაითვალოს ეს თავდასხმად
|
||||
# ეს რიცხვი მოქმედებს ყალბი თავდასხმის შეტყობინებების გასაფილტრად
|
||||
min-attack-threshold: 2
|
||||
# დრო, რომელიც უნდა გავიდეს ახალი შეტევის დასადგენად
|
||||
@@ -38,9 +41,11 @@ attack-tracker:
|
||||
# თქვენ შეგიძლიათ დააკავშიროთ Sonar მონაცემთა ბაზას, რომ შეინახოთ ვერიფირებული მოთამაშეები თქვენი სერვერის გადატვირთვის შემდეგაც კი
|
||||
# შენიშვნა: IP მისამართები ინახება უბრალო ტექსტში. თქვენ ხართ პასუხისმგებელი თქვენი მონაცემთა ბაზის უსაფრთხოებაზე!
|
||||
database:
|
||||
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს ვერიფირებული მოთამაშეების შესანახად
|
||||
# შესაძლო ტიპები: NONE, MYSQL, MARIADB
|
||||
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს უკვე შემოწმებული მოთამაშეების შესანახად
|
||||
# შესაძლო ტიპები: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# ფაილი სონარ_ის ფოლდერში რომელიც ინახავს H2 database_ს
|
||||
filename: verified_players
|
||||
# ჰოსტის მისამართი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
host: localhost
|
||||
# პორტი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
@@ -51,35 +56,35 @@ database:
|
||||
username: ''
|
||||
# პაროლი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
password: ''
|
||||
# რამდენი დღე უნდა შეინახოს Sonar-მა დამოწმებული მოთამაშეები მონაცემთა ბაზაში?
|
||||
# რამდენი დღე უნდა შეინახოს Sonar-მა შემოწმებული მოთამაშეები მონაცემთა ბაზაში?
|
||||
maximum-age: 5
|
||||
|
||||
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ სპამის მიერთების შეტევები
|
||||
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ შემოსვლის სპამის შეტევები
|
||||
# რიგი განახლდება ყოველ 1000 მილიწამში (20 ტიკი)
|
||||
queue:
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 1000 მილიწამში
|
||||
max-polls: 30
|
||||
|
||||
# ყოველი ახალი მოთამაშე, რომელიც პირველად შეუერთდება, გაიგზავნება
|
||||
# ყოველი ახალი მოთამაშე, რომელიც პირველად შემოვა, გაიგზავნება
|
||||
# ლიმბო სერვერში, სადაც მოთამაშეების ვერიფიკაცია ხდება
|
||||
verification:
|
||||
# როდის უნდა გადაამოწმოს Sonar-მა ახალი მოთამაშეები?
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოწმდება (რეკომენდებულია)
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროსდროს
|
||||
# - NEVER: ახალი მოთამაშეები არასოდეს შემოწმდებიან
|
||||
# - ALWAYS (ყოველთვის): ახალი მოთამაშეები ყოველთვის მოწმდება (რეკომენდებულია)
|
||||
# - DURING_ATTACK (შეტევის დროს): ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროს
|
||||
# - NEVER (არასდროს): ახალი მოთამაშეები არასდროს შემოწმდებიან
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# ამოწმებს, მიჰყვება თუ არა მოთამაშეების დაცემის მოძრაობა Minecraft-ის გრავიტაციის ფორმულას
|
||||
# ყველა პროგნოზირებული მოძრაობა წინასწარ არის გათვლილი პერფორმანსის შესანარჩუნებლად
|
||||
# ამოწმებს, მიჰყვება თუ არა მოთამაშეების ვარდნის მოძრაობა Minecraft-ის გრავიტაციის ფორმულას
|
||||
# ყველა პროგნოზირებული მოძრაობა წინასწარ არის გათვლილი შესრულების სისწრაფის შესანარჩუნებლად
|
||||
gravity:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა კლიენტის გრავიტაცია? (რეკომენდირებულია)
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა მოთამაშის (ან ბოტის) გრავიტაცია? (რეკომენდებულია)
|
||||
enabled: true
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
|
||||
check-collisions: true
|
||||
# უნდა აიძულოს თუ არა Sonar-მა მოთამაშეს გადაჭრას CAPTCHA, თუ ისინი ვერ შეასრულებენ სიმძიმის შემოწმებას?
|
||||
# უნდა აიძულოს თუ არა Sonar-მა მოთამაშეს შეასრულოს CAPTCHA, თუ ისინი ვერ შეასრულებენ სიმძიმის შემოწმებას?
|
||||
captcha-on-fail: false
|
||||
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
|
||||
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს. (20 ტიკი = 1 წამი)
|
||||
max-movement-ticks: 8
|
||||
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
|
||||
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
|
||||
@@ -91,20 +96,20 @@ verification:
|
||||
# ამოწმებს, აგზავნის თუ არა კლიენტი შესაბამის პაკეტებს სატრანსპორტო საშუალებაზე დაჯდომის დროს
|
||||
vehicle:
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის დაჯდებიან სატრანსპორტო საშუალებაზე
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები მხოლოდ ბოტების შემოტევის დროს დაჯდებიან სატრანსპორტო საშუალებაზე
|
||||
# - NEVER: ახალი მოთამაშეები არასოდეს დაჯდებიან სატრანსპორტო საშუალებაზე (რეკომენდებულია)
|
||||
# - ALWAYS (ყოველთვის): ახალი მოთამაშეები ყოველთვის დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე
|
||||
# - DURING_ATTACK (შეტევის დროს): ახალი მოთამაშეები მხოლოდ შეტევის დროს დაჯდებიან დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე
|
||||
# - NEVER (არასდროს): ახალი მოთამაშეები არასოდეს დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე (რეკომენდებულია)
|
||||
timing: ALWAYS
|
||||
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებათ
|
||||
map-captcha:
|
||||
# როდის უნდა გააკეთებინოს Sonar-მა მოთამაშეს CAPTCHA?
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოუწევთ CAPTCHA-ს გაკეთება
|
||||
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
|
||||
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
|
||||
# - ALWAYS (ყოველთვის): ახალი მოთამაშეები ყოველთვის მოუწევთ CAPTCHA-ს გაკეთება.
|
||||
# - DURING_ATTACK (შეტევის დროს): ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს.
|
||||
# - NEVER (არასდროს): ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended).
|
||||
timing: NEVER
|
||||
# ეფექტების პარამეტრები გამოსახულებაზე, რომელიც აჩვენა მოთამაშეს
|
||||
# ეფექტების პარამეტრები Captcha გამოსახულებაზე, რომელსაც ვანახებთ მოთამაშეს
|
||||
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
|
||||
effects:
|
||||
# მატებს შემთხვევითი ხაზების ეფექტს გამოსახულებაზე
|
||||
@@ -124,9 +129,9 @@ verification:
|
||||
# - 2: წრეები
|
||||
# - 3: კვადრატები
|
||||
shape: 2
|
||||
# მანძილი დამახინჯებულ პიქსელებს/ობიექტებს შორის
|
||||
# მანძილი არეულ პიქსელებს/ობიექტებს შორის
|
||||
distance: 5
|
||||
# დამახინჯებული პიქსელების/ობიექტების სიხშირე
|
||||
# არეული პიქსელების/ობიექტების სიხშირე
|
||||
density: 0.1
|
||||
# შერევის ხარისხი ობიექტების ფერებს შორის
|
||||
mix: 0.15
|
||||
@@ -139,7 +144,7 @@ verification:
|
||||
# თუ ეს გამორთულია, Sonar-ი გამოიყენებს შემთხვევით გრადიენტს ფერებისთვის
|
||||
auto-color: false
|
||||
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
|
||||
# ეს ხდება ასინქრონულად უკანა ფონზე
|
||||
# ეს ხდება ასინქრონულად (ანუ ამის გამოთვლას სერვერი არ დაელოდება) უკანა ფონზე
|
||||
precompute: 500
|
||||
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
@@ -150,32 +155,32 @@ verification:
|
||||
# არ არის რეკომენდებული რიცხვების ან ასოების დამატება, რომლებიც შეიძლება ერთმანეთს ჰგავდეს
|
||||
dictionary: '1235689'
|
||||
|
||||
# ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
|
||||
# ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ მოთამაშის მეტსახელს
|
||||
client-brand:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა ნამდვილია თუ არა მოთამაშის კლიენტის სახელი? (რეკომენდებულია)
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა ნამდვილია თუ არა მოთამაშის სახელი? (რეკომენდებულია)
|
||||
enabled: true
|
||||
# Regex-ი კლიენტის ბრენდების დასადასტურებლად ვერიფიკაციის დროს
|
||||
# Regex-ი მოთამაშის ბრენდების დასადასტურებლად ვერიფიკაციის დროს
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# მოთამაშის კლიენტის სახელის მაქსიმალური სიგრძე ვერიფიკაციის დროს
|
||||
max-length: 64
|
||||
|
||||
# Regex-ი მომხმარებლის სახელების ვერიფიკაციისთვის
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex-ი კლიენტის ლოკალის დასადასტურებლად ვერიფიკაციის დროს
|
||||
# Regex-ი კლიენტის ადგილის დასადასტურებლად ვერიფიკაციის დროს
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# პინგი რამდენიც უნდა ქონდეს მოთამაშეს იმისთვის, რომ ტაიმაუტდეს
|
||||
# პინგი რამდენიც უნდა ქონდეს მოთამაშეს იმისთვის, რომ დროებით დაიბლოკოს (გავარდეს)
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
max-ping: 10000
|
||||
# მაქსიმალური რაოდენობა ლოგინ პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გაიკიკება სერვერიდან
|
||||
# მაქსიმალური რაოდენობა შესვლის (LOGIN) პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გავარდება სერვერიდან
|
||||
max-login-packets: 256
|
||||
|
||||
# თუ ჩართულია, მოთამაშე გადავა საწყისი სერვერში ერიფიკაციის წარმატებით გავლის შემდეგ
|
||||
# თუ ჩართულია, მოთამაშე გადავა საწყის სერვერში ვერიფიკაციის წარმატებით გავლის შემდეგ
|
||||
# ეს ფუნქცია დაემატა Mojang-ის მიერ Minecraft-ის 1.20.5 ვერსიაში
|
||||
transfer:
|
||||
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაკიკვის ნაცვლად)?
|
||||
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაგდების ნაცვლად)?
|
||||
# ამისთვის, თქვენ უნდა ჩართოთ ეს ფუნქცია თქვენი სერვერის კონფიგურაციაში
|
||||
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის rate-limiting ი
|
||||
# რომელსაც ახორციელებს Velocity ან სხვა პროქსები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
|
||||
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის სიხშირის ლიმიტი
|
||||
# რომელსაც ახორციელებს Velocity ან სხვა პროქსიები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
|
||||
# მოთამაშის სწორად გადაყვანას
|
||||
enabled: false
|
||||
# რომელ სერვერზე უნდა გადაიყვანოს Sonar-მა მოთამაშე ვერიფიკაციის გავლის შემდეგ?
|
||||
@@ -191,15 +196,18 @@ verification:
|
||||
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
|
||||
log-during-attack: false
|
||||
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
|
||||
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
|
||||
# ეს არ არის რეკომენდებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
|
||||
debug-xyz-positions: false
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა Geyser (Bedrock) მოთამაშეები?
|
||||
# ეს ფუნქცია ექსპერიმენტულია და შეიძლება გამოიწვიოს პრობლემები
|
||||
# თუ ეს გამორთულია, Bedrock მოთამაშეები გამოვლენილი იქნება
|
||||
check-geyser-players: true
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სერვერთან კავშირის გარეშე, სანამ მოთამაშე დროებით გავარდება სერვერიდან.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
read-timeout: 8000
|
||||
# დრო როდიდანაც სერვერმა შეწყვიტა პაკეტების გაგზავნა სანამ გააგდებს მოთამაშეს.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
write-timeout: 10000
|
||||
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
# Algemene instellingen voor Sonar
|
||||
general:
|
||||
# Moet Sonar automatisch controleren op nieuwe updates?
|
||||
# Sonar gebruikt de GitHub API om te controleren of de huidige versie verouderd is
|
||||
check-for-updates: true
|
||||
# Moet Sonar de IP-adressen van spelers in de console registreren?
|
||||
log-player-addresses: true
|
||||
# Maximaal aantal spelers online met hetzelfde IP-adres
|
||||
@@ -39,8 +42,10 @@ attack-tracker:
|
||||
# Let op: IP-adressen worden opgeslagen in platte tekst. U bent verantwoordelijk voor het veilig houden van uw database!
|
||||
database:
|
||||
# Type database dat Sonar gebruikt om geverifieerde spelers op te slaan
|
||||
# Mogelijke typen: NONE, MYSQL, MARIADB
|
||||
# Mogelijke typen: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Bestand in de plugin-directory van Sonar dat wordt gebruikt voor de H2-database
|
||||
filename: verified_players
|
||||
# Adres voor authenticatie met de SQL-database
|
||||
host: localhost
|
||||
# Poort voor authenticatie met de SQL-database
|
||||
@@ -201,9 +206,12 @@ verification:
|
||||
# De hoeveelheid tijd die moet verstrijken voordat een speler een time-out krijgt
|
||||
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
|
||||
read-timeout: 8000
|
||||
# Hoe lang moet een speler wachten voordat hij tijdens de verificatie opnieuw verbinding maakt?
|
||||
# De tijd sinds de speler stopte met het verzenden van pakketten voordat de verbinding werd verbroken
|
||||
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
|
||||
rejoin-delay: 8000
|
||||
# De tijd sinds de server stopte met het versturen van pakketten voordat de verbinding met de speler werd verbroken
|
||||
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
|
||||
write-timeout: 10000
|
||||
# Hoe lang moet Sonar het aantal mislukte verificaties voor een speler onthouden?
|
||||
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
|
||||
remember-time: 120000
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Potrzebujesz pomocy lub masz pytania? https://jonesdev.xyz/discord
|
||||
# Source code: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# Ustawienia ogólne Sonar
|
||||
general:
|
||||
# Czy Sonar powinien automatycznie sprawdzać dostępność nowych wersji?
|
||||
# Sonar korzysta z API GitHub, aby sprawdzić, czy bieżąca wersja jest nieaktualna
|
||||
check-for-updates: true
|
||||
# Czy Sonar powinien rejestrować adresy IP graczy w konsoli?
|
||||
log-player-addresses: true
|
||||
# Maksymalna liczba graczy online z tym samym adresem IP
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Ustawienia czułości dla funkcji śledzenia ataku
|
||||
# Edytuj to tylko wtedy, gdy wiesz, co robisz
|
||||
attack-tracker:
|
||||
# Minimalna liczba nowych graczy do wykrycia ataku
|
||||
min-players-for-attack: 8
|
||||
# Czas, który musi upłynąć, aby tryb ataku się zakończył.
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
min-attack-duration: 30000
|
||||
# Ile razy incydent musi zostać zgłoszony, aby został uznany za atak?
|
||||
# Liczba ta działa jak bufor do filtrowania fałszywych powiadomień o atakach
|
||||
min-attack-threshold: 2
|
||||
# Czas, który musi upłynąć, aby nowy atak mógł zostać wykryty.
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# Sonar można podłączyć do bazy danych, aby zachować zweryfikowanych graczy nawet po ponownym uruchomieniu serwera.
|
||||
# Uwaga: Adresy IP są zapisywane w postaci zwykłego tekstu. Jesteś odpowiedzialny za bezpieczeństwo swojej bazy danych!
|
||||
database:
|
||||
# Typ bazy danych używanej przez Sonar.
|
||||
# Dostępne rodzaje: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Plik w folderze pluginu używany dla bazy danych H2
|
||||
filename: verified_players
|
||||
# Adres hosta do uwierzytelniania w bazie danych SQL
|
||||
host: localhost
|
||||
# Port do uwierzytelniania z bazą danych SQL
|
||||
port: 3306
|
||||
# Nazwa bazy danych SQL
|
||||
name: sonar
|
||||
# Nazwa użytkownika do uwierzytelniania w bazie danych SQL
|
||||
username: ''
|
||||
# Hasło do uwierzytelniania w bazie danych SQL
|
||||
password: ''
|
||||
# Przez ile dni Sonar powinien przechowywać zweryfikowanych graczy w bazie danych?
|
||||
maximum-age: 5
|
||||
|
||||
# Każde nowe logowanie będzie kolejkowane w celu uniknięcia ataków typu spam join.
|
||||
# Kolejka jest aktualizowana co 1 sekundę (20 tików).
|
||||
queue:
|
||||
# Maksymalna liczba jednoczesnych odpytywań kolejki na 1 sekundę
|
||||
max-polls: 30
|
||||
|
||||
# Każdy nowy gracz, który dołączy po raz pierwszy, zostanie wysłany do
|
||||
# lekkiego serwera Limbo, na którym przeprowadzane są zaawansowane kontrole antybotowe
|
||||
verification:
|
||||
# Kiedy Sonar powinien weryfikować nowych graczy?
|
||||
# Możliwe rodzaje: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Nowi gracze będą zawsze weryfikowani (zalecane)
|
||||
# - DURING_ATTACK: Nowi gracze będą weryfikowani tylko podczas ataku
|
||||
# - NEVER: Nowi gracze nigdy nie będą weryfikowani
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Sprawdza, czy ruch spadający gracza jest zgodny z działaniem grawitacji Minecraft.
|
||||
# Wszystkie przewidywane ruchy są wstępnie obliczane w celu zachowania wydajności
|
||||
gravity:
|
||||
# Czy Sonar powinien sprawdzać poprawność grawitacji gracza? (Zalecane)
|
||||
enabled: true
|
||||
# Czy Sonar powinien sprawdzać, czy gracz zderza się z blokami? (Zalecane)
|
||||
check-collisions: true
|
||||
# Czy Sonar powinien zmusić gracza do rozwiązania CAPTCHA, jeśli nie uda mu się przejść testu grawitacji?
|
||||
captcha-on-fail: false
|
||||
# Maksymalna liczba tików, po których gracz musi spaść, aby móc uderzyć w platformę.
|
||||
max-movement-ticks: 8
|
||||
# Tryb gry gracza (gamemode) podczas weryfikacji
|
||||
# Dostępne rodzaje: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: Wszystkie komponenty interfejsu są widoczne
|
||||
# - CREATIVE: pasek życia i głodu jest ukryty
|
||||
# - ADVENTURE: wszystkie komponenty interfejsu są widoczne
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# Sprawdza, czy gracz wysyła odpowiednie pakiety podczas umieszczania na pojeździe.
|
||||
vehicle:
|
||||
# Dostępne rodzaje: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Nowi gracze będą zawsze umieszczani na pojeździe (zalecane).
|
||||
# - DURING_ATTACK: Nowi gracze będą umieszczani na pojeździe tylko podczas ataku.
|
||||
# - NEVER: Nowi gracze nigdy nie zostaną umieszczeni na pojeździe.
|
||||
timing: ALWAYS
|
||||
|
||||
# Zmuszenie gracza do wpisania kodu z wirtualnej mapy na czacie (CAPTCHA).
|
||||
map-captcha:
|
||||
# Kiedy Sonar powinien zmusić gracza do rozwiązania CAPTCHA?
|
||||
# Dostępne rodzaje: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Nowi gracze zawsze otrzymają weryfikacje CAPTCHA
|
||||
# - DURING_ATTACK: Nowi gracze otrzymają weryfikacje CAPTCHA tylko podczas ataku.
|
||||
# - NEVER: Nowi gracze nigdy nie otrzymają weryfikacji CAPTCHA (zalecane)
|
||||
timing: NEVER
|
||||
# Opcje efektów na mapie wyświetlanej graczowi
|
||||
# Powinieneś to edytować tylko wtedy, gdy wiesz, co robisz
|
||||
effects:
|
||||
# Dodawanie losowych linii do obrazu
|
||||
scratches: true
|
||||
# Stosuje efekt falowania
|
||||
ripple: true
|
||||
# Stosuje filtr nierówności (wyostrzony tekst)
|
||||
bump: true
|
||||
# Filtr zniekształceń
|
||||
distortion:
|
||||
# Czy Sonar powinien stosować filtr zniekształceń?
|
||||
enabled: true
|
||||
# Jakiego kształtu filtra zniekształceń powinien używać Sonar?
|
||||
# Dostępne rodzaje: 0, 1, 2, 3
|
||||
# - 0: Krzyże
|
||||
# - 1: Linie
|
||||
# - 2: Kółka
|
||||
# - 3: Kwadraty
|
||||
shape: 2
|
||||
# Odległość między zniekształconymi pikselami/obiektami
|
||||
distance: 5
|
||||
# Gęstość zniekształconych pikseli/obiektów
|
||||
density: 0.1
|
||||
# Stopień mieszania się kolorów obiektów
|
||||
mix: 0.15
|
||||
# Ścieżka do obrazu tła używanego przez Sonar dla CAPTCHA
|
||||
# Jeśli obraz nie zostanie znaleziony, jako domyślne tło zostanie użyty szum
|
||||
# Możesz umieścić nazwę pliku tutaj, jeśli plik znajduje się w katalogu pluginu
|
||||
background: ''
|
||||
# Czy Sonar powinien automatycznie wykrywać kolor tła?
|
||||
# Ma to na celu stworzenie kontrastu między obrazem tła a tekstem
|
||||
# Jeśli ta opcja jest wyłączona, Sonar użyje losowego gradientu dla kolorów
|
||||
auto-color: false
|
||||
# Ile możliwych odpowiedzi powinien przygotować Sonar?
|
||||
# Zadanie to wykonywane jest asynchronicznie w tle
|
||||
precompute: 500
|
||||
# Jak długo Sonar powinien czekać, aż gracz nie przepisze kodu CAPTCHA, zanim go wyrzuci?
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
max-duration: 30000
|
||||
# Ile razy gracz musi przepisać błędnie kod CAPTCHA przed wyrzuceniem z serwera?
|
||||
max-tries: 3
|
||||
# Znaki (litery i cyfry), które mogą pojawić się w CAPTCHA
|
||||
# Nie zaleca się dodawania cyfr lub liter, które mogą być do siebie podobne
|
||||
dictionary: '1235689'
|
||||
|
||||
# Sprawdza, czy gracz wysyła prawidłowy client brand do serwera.
|
||||
client-brand:
|
||||
# Czy Sonar powinien sprawdzać poprawność client brand? (Zalecane)
|
||||
enabled: true
|
||||
# Regex do sprawdzania poprawności client brand podczas weryfikacji
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# Maksymalna długość wartości client brand podczas weryfikacji
|
||||
max-length: 64
|
||||
|
||||
# Regex do sprawdzania poprawności nazw użytkowników podczas weryfikacji
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex do sprawdzania ustawień regionalnych klienta podczas weryfikacji
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maksymalny ping gracza, zanim zostanie wyrzucony z serwera.
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
max-ping: 10000
|
||||
# Liczba pakietów logowania, które gracz musi wysłać, aby zostać wyrzuconym.
|
||||
max-login-packets: 256
|
||||
|
||||
# Jeśli opcja ta jest włączona, gracz zostanie przeniesiony z powrotem
|
||||
# na serwer początkowy po pomyślnym przejściu weryfikacji antybotowej.
|
||||
# Funkcja ta została wprowadzona przez Mojang w wersji 1.20.5
|
||||
transfer:
|
||||
# Czy Sonar powinien przenieść gracza na serwer początkowy (zamiast go wyrzucać)?
|
||||
# Aby to zadziałało, musisz włączyć tę funkcję w konfiguracji serwera.
|
||||
# Dodatkowo, możesz chcieć zmniejszyć limitowanie prędkości logowania (ratelimit).
|
||||
# wykonywane przez Velocity lub inne silniki/pluginy, ponieważ
|
||||
# może to uniemożliwić prawidłowe przeniesienie gracza.
|
||||
enabled: false
|
||||
# Na który serwer Sonar powinien przenieść gracza po pomyślnym przejściu weryfikacji?
|
||||
# Wprowadź adres IP serwera, który jest używany przez graczy do normalnego łączenia się z serwerem.
|
||||
# Na przykład, możesz wpisać "mc.hypixel.net" lub bezpośredni adres IP, taki jak "1.1.1.1".
|
||||
destination-host: "play.my-server.com"
|
||||
# Jaki port powinien używać Sonar podczas przenoszenia gracza do serwera początkowego?
|
||||
# Jeśli serwer nie potrzebuje portu do połączenia, można pozostawić 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# Czy Sonar powinien rejestrować połączenia nowych graczy?
|
||||
log-connections: true
|
||||
# Czy Sonar powinien rejestrować nowe próby połączenia podczas ataku?
|
||||
log-during-attack: false
|
||||
# Czy sonar powinien zapisywać każdy ruch/zmianę pozycji podczas weryfikacji?
|
||||
# Nie jest to zalecane dla serwerów publicznych, ale może być pomocne w wykrywaniu błędów.
|
||||
debug-xyz-positions: false
|
||||
# Czy Sonar powinien również sprawdzać graczy Geyser (Bedrock)?
|
||||
# Ta funkcja jest eksperymentalna i może powodować problemy
|
||||
# Jeśli ta opcja jest wyłączona, gracze Bedrock będą pomijani
|
||||
check-geyser-players: true
|
||||
# Czas, jaki ma upłynąć od zatrzymania wysyłania pakietów przez gracza do wyrzucenia.
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
read-timeout: 8000
|
||||
# Czas, jaki ma upłynąć od zatrzymania wysyłania pakietów przez serwer do wyrzucenia.
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
write-timeout: 10000
|
||||
# Jak długo niezweryfikowany gracz powinien czekać przed ponownym połączeniem do serwera?
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
rejoin-delay: 8000
|
||||
# Jak długo Sonar powinien zapamiętywać liczbę nieudanych weryfikacji gracza?
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
remember-time: 120000
|
||||
# Na jak długo Adres IP ma zostać umieszczony na czarnej liście, jeśli przekroczył liczbe nieudanych weryfikacji?
|
||||
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
|
||||
blacklist-time: 600000
|
||||
# Jak często gracz ma nie przechodzić weryfikacji, aby znaleźć się na czarnej liście?
|
||||
# (Ustaw tę wartość na 0, aby całkowicie wyłączyć czarną listę).
|
||||
blacklist-threshold: 2
|
||||
# Lista protokołów (wersji gry), które nie będą sprawdzane przez Sonar
|
||||
# Pełną listę wszystkich protokołów można znaleźć tutaj:
|
||||
# https://wiki.vg/Protocol_version_numbers
|
||||
# Na przykład Minecraft 1.20 ma protokół 763.
|
||||
whitelisted-protocols: []
|
||||
# Lista protokołów, które nie mogą w ogóle dołączyć do serwera
|
||||
blacklisted-protocols: []
|
||||
@@ -15,6 +15,9 @@
|
||||
|
||||
# Основные настройки
|
||||
general:
|
||||
# Должен ли Sonar автоматически проверять наличие новых обновлений?
|
||||
# Sonar использует API GitHub, чтобы проверить, не устарела ли текущая версия
|
||||
check-for-updates: true
|
||||
# Должен ли Sonar логировать IP-адреса игроков в консоли?
|
||||
log-player-addresses: true
|
||||
# Максимальное количество игроков онлайн с одного IP-адреса
|
||||
@@ -39,8 +42,10 @@ attack-tracker:
|
||||
# Примечание: IP-адреса сохраняются в виде обычного текста. Вы сами отвечаете за сохранность своей базы данных!
|
||||
database:
|
||||
# Тип базы данных, которую Sonar использует для хранения проверенных игроков.
|
||||
# Возможные типы: NONE, MYSQL, MARIADB
|
||||
# Возможные типы: NONE, MYSQL, MARIADB, H2
|
||||
type: NONE
|
||||
# Файл в каталоге плагинов Sonar, используемый для базы данных H2
|
||||
filename: verified_players
|
||||
# Адрес хоста для аутентификации в базе данных SQL
|
||||
host: localhost
|
||||
# Порт для аутентификации с базой данных SQL
|
||||
@@ -197,13 +202,16 @@ verification:
|
||||
# Эта функция экспериментальна и может вызывать проблемы
|
||||
# Если это отключено, игроки Bedrock будут пропущены
|
||||
check-geyser-players: true
|
||||
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
|
||||
# Время, прошедшее с момента прекращения отправки пакетов игроком до отключения.
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
read-timeout: 8000
|
||||
# Время, прошедшее с момента прекращения отправки сервером пакетов до отключения игрока
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
write-timeout: 10000
|
||||
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# Как долго Sonar должен помнить количество неудачных проверок для игрока?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
remember-time: 120000
|
||||
# Как долго IP-адресу должно быть запрещено входить на сервер, если он слишком часто не проходит проверку?
|
||||
|
||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user