Compare commits

...
27 Commits
Author SHA1 Message Date
Jones 0137fdf7e9 Release 2.1.0 (#361) 2024-08-10 14:25:39 +02:00
Jones df14e42a0b Improve the collision check (#360)
* feat: add iron trapdoor block

* feat: add enchantment table block

* fix: rename Captcha to CAPTCHA

* feat: randomize vehicle entity ID

* feat: make collision platform block type configurable

* fix: only send experience bar if it's visible

* feat: change vehicle "timing" to "enabled"

* fix(fallback): captcha-on-fail not catching every scenario

* style: oopsie doopsie
2024-08-10 14:08:28 +02:00
Michel Elkenwaat c30dd0225b Implement missing "fell through blocks" check 2024-08-09 04:22:31 +02:00
Michel Elkenwaat 626fa422bc Fix some grammar mistakes in comments 2024-08-09 01:32:59 +02:00
Michel Elkenwaat f08d9239fe Fix verification.currently-preparing not being used 2024-08-08 23:29:14 +02:00
Michel Elkenwaat 1edd71df04 Fix Bedrock players not being able to be transferred (https://github.com/jonesdevelopment/sonar/issues/356) 2024-08-08 22:28:39 +02:00
aff3bd4a64 Implement Bukkit Support (#347)
* feat: deject packet listener if not needed anymore

* fix(db): dynamically load drivers libraries

* refactor: outline VerifiedPlayerController#exactSize

* fix(db): small synchronization issues in VerifiedPlayerController

* feat: enforce Netty 4.1.x (or higher)

* feat: [wip] Bukkit injection (NMS/OBC stuff)

* fix: include MIT license header for fast varint reading

* fix: allow Netty version to remain unknown

* fix: optimize "command sender -> audience" logic

* fix: extremely rare NoSuchElementException

* fix: remove equals and hashcode annotations

* build(deps): add annotations and simpleyaml to catalog

* feat: [wip] implement Bukkit injection

* feat: [wip] Bukkit packet reading and general improvements

* refactor: small code style improvements

* style: link wiki.vg in comments

* fix: don't store unnecessary pipeline names

* fix: transfer state not working on Bukkit

* fix: possible bypass with the transfer mode exemption

* fix(bukkit): library folder having the wrong name

* feat: make cached player username configurable

* fix: add missing configuration entry to pl.yml

* fix: pipeline order breaking everything ¯\_(ツ)_/¯

* fix: exempt geyser pipelines from being removed... oops

* fix: injection error on 1.20.5+ servers

* build(bukkit): use 1.13.2 spigot api instead of 1.8.8

* fix: legacy plugin warning by setting the api version to 1.13

* fix: use entity type in SpawnEntityPacket

* feat: improve the vehicle check by a lot

* fix: pipeline exception tail error

* fix: only compute player if actually connected (https://github.com/jonesdevelopment/sonar/issues/349)

* style: static import consistency

* fix: MessageToMessageDecoder literally breaking HAProxy

* feat: add more bStats metrics (language and db type)

* style: small code clean up

* feat: remove unnecessary plugin-yml generation arguments

* fix(bukkit): use the actual server name for debugging

* feat: don't shadow adventure-nbt

* chore: link transfer tutorial in configuration

* fix: CI pipeline build error

* build: mark all gradle tasks as non-deamon

* fix: vehicle check 2.0 not being compatible with Geyser

* refactor: *PacketDecoder -> *InboundHandler

* fix: SetPlayerPositionRotationPacket onGround not being used

* feat: recode vehicle check to work with laggy connections

* fix: remove debug message(s)

* feat: recode vehicle check again

* fix: don't remove the entity twice

* feat: [wip] implement Bukkit support (it's looking good)

* fix: wrong exception being caught

* fix: improve error handling in reflective operations

* feat: optimize verification routines

* feat: optimize and improve queue computing

* fix: double/float comparison being broken haha

* style: small comment improvements

* style: fix invalid comments in vehicle check

* Bug fixes in `bukkit-support` branch (#358)

* Lazy load for ServerConnection to support late-bind

* Remove `sonar-inbound-handler` when join server

* Add missing license header in `Lazy.java`

* Update FallbackBukkitInjector

* Change `Bukkit#getScheduler` to `getPlugin().getServer().getScheduler()`

* Inject the new initializer to close connection if the plugin is not enabled.

* Don't remove `DefaultChannelPipeline$TailContext#0` in pipeline.

* Add listener for close uninject player's connection if `late-bind` is disabled.

* Fixed an issue where `max-online-per-ip` was not working properly.

* Get `ServerConnection` in method.

* Change overwrite methods position.

* Move `ChannelInactiveListener` to `sonar-api`

* style(bukkit): create unreflected fields in a static context

* style(bukkit): use lombok Getter for late bind

* build: fix compilation process

* refactor: move `INITIALIZE_LISTENER` from SonarBukkitPlugin to SonarBukkit

* style(bukkit): improve codestyle in the inbound handler

* style: fix wrong comment

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>

* style: add missing credits

* build(deps): bump adventure-platform from 4.3.3 to 4.3.4

* fix: don't assign unused fields

* fix: use encoder function instead of manual encoder check

* fix: use encoder function, not handler function x)

* fix: optimize bukkit encoder function by using `context`, not `get`

* fix: memory leak in the login packet decoder

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2024-08-08 21:13:16 +02:00
Michel Elkenwaat ce1d623d60 Allow general.max-online-per-ip to be 0 2024-08-03 19:29:15 +02:00
Jones 77716c0fcc Release 2.0.38 (#357) 2024-07-30 19:56:39 +02:00
dependabot[bot] c34a3d7dc2 Bump netty-all from 4.1.111.Final to 4.1.112.Final (#355)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.111.Final to 4.1.112.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.111.Final...netty-4.1.112.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 17:17:21 +02:00
Michel Elkenwaat 4fdb7cc70c Fix IP addresses being parsed incorrectly 2024-07-19 15:39:57 +02:00
slosacoder 7ddca3e152 Update Czech translations (#354) 2024-07-16 18:40:49 +02:00
Michel Elkenwaat e01a226475 Bump Gradle from 8.8 to 8.9 2024-07-15 19:36:38 +02:00
Michel Elkenwaat 21d983362e Fix an invalid character in the Georgian translations 2024-07-12 19:00:16 +02:00
Michel Elkenwaat ab5203f4c5 Fix a small translation error 2024-07-12 18:59:32 +02:00
GreenedDev 03c209829c Fix grammar errors in Georgian config (#350) 2024-07-12 16:33:32 +02:00
GreenedDev af57d6de42 Add Georgian messages translations (#351) 2024-07-12 16:32:31 +02:00
Michel Elkenwaat d1f8ab4f10 Add Polish translations (thanks to Najek and foren) 2024-07-11 19:14:09 +02:00
Jones c12d77fb8d Release 2.0.37 (#346) 2024-07-04 16:24:03 +02:00
Michel Elkenwaat a9a23aaf2f Fix client brand reading (Revert https://github.com/jonesdevelopment/sonar/commit/e0cc9bdab3ee93a5c328f3015bd3564f31ed7e0c) 2024-07-04 16:19:11 +02:00
Jones 58d0c57ee8 Release 2.0.36 (#345) 2024-07-03 14:47:01 +02:00
Michel Elkenwaat 5546d2695d Add Indonesian translations (thanks to LourenT) 2024-07-02 14:19:43 +02:00
dependabot[bot] 64688ba06b Bump lombok from 1.18.32 to 1.18.34 (#343)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:23:27 +02:00
dependabot[bot] e85036dd13 Bump io.github.goooler.shadow from 8.1.7 to 8.1.8 (#344)
Bumps [io.github.goooler.shadow](https://github.com/Goooler/shadow) from 8.1.7 to 8.1.8.
- [Release notes](https://github.com/Goooler/shadow/releases)
- [Commits](https://github.com/Goooler/shadow/compare/v8.1.7...v8.1.8)

---
updated-dependencies:
- dependency-name: io.github.goooler.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:23:08 +02:00
Michel Elkenwaat 2b2b481c14 Globalize and simplify per-platform injection 2024-06-30 13:41:07 +02:00
Michel Elkenwaat e0cc9bdab3 Simplify client brand reading for 1.8+ 2024-06-30 13:17:07 +02:00
Jones 8aa05563d5 Add support for H2 databases (#342)
* build(deps): bump MySQL driver from 8.3.0 to 8.4.0

* build(deps): bump mariadb-java-client from 3.3.3 to 3.4.0

* feat: add h2 local database support

* style: fix javadoc for db type adapters [ci skip]
2024-06-30 12:40:28 +02:00
87 changed files with 4099 additions and 931 deletions

No files matched your search

+1
View File
@@ -49,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/*/**")
+2 -1
View File
@@ -1,3 +1,4 @@
org.gradle.daemon=false
group=xyz.jonesdev.sonar
version=2.0.35
version=2.1.0
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+7 -7
View File
@@ -1,13 +1,13 @@
[versions]
netty = "4.1.111.Final"
netty = "4.1.112.Final"
adventure = "4.17.0"
adventure-platform = "4.3.3"
adventure-platform = "4.3.4"
bstats = "3.0.2"
libby = "2.0.0-SNAPSHOT"
[plugins]
indra-git = "net.kyori.indra.git:3.1.3"
shadow = "io.github.goooler.shadow:8.1.7"
shadow = "io.github.goooler.shadow:8.1.8"
[libraries]
netty = { module = "io.netty:netty-all", version.ref = "netty" }
@@ -16,8 +16,6 @@ adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", ve
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
adventure-platform-facet = { module = "net.kyori:adventure-platform-facet", version.ref = "adventure-platform" }
adventure-platform-api = { module = "net.kyori:adventure-platform-api", version.ref = "adventure-platform" }
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }
@@ -26,10 +24,12 @@ bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats
bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
annotations = "org.jetbrains:annotations:24.0.1"
simpleyaml = "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
lombok = "org.projectlombok:lombok:1.18.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"
# We have to use 1.8 for backwards compatibility
# We have to use 1.8.8 for backwards compatibility
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+5 -2
View File
@@ -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
View File
@@ -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 ##########################################################################
+2 -3
View File
@@ -3,11 +3,10 @@ repositories {
}
dependencies {
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
compileOnly(rootProject.libs.simpleyaml) {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
compileOnly(rootProject.libs.annotations)
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -46,6 +46,9 @@ public interface Sonar {
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* @return The platform the plugin is being run on
*/
@@ -17,19 +17,30 @@
package xyz.jonesdev.sonar.api;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110),
BUNGEE("BungeeCord", 19109),
VELOCITY("Velocity", 19107);
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
}
@@ -21,7 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
enum Language {
public enum Language {
SYSTEM("system"),
AB("Abkhazian"),
AA("Afar"),
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.config;
import com.alessiodp.libby.Library;
import com.j256.ormlite.db.DatabaseType;
import lombok.*;
import net.kyori.adventure.text.Component;
@@ -27,6 +28,7 @@ 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,7 +47,10 @@ import java.util.regex.Pattern;
public final class SonarConfiguration {
@Getter
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
@Getter
private final File languageFile, pluginFolder;
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -119,27 +124,27 @@ public final class SonarConfiguration {
}
// Generate the language file and check what it's set to
Language preferredLanguage = getPreferredLanguage();
if (preferredLanguage == Language.SYSTEM) {
language = getPreferredLanguage();
if (language == Language.SYSTEM) {
try {
// Try using the system language to determine the language file
final String property = System.getProperty("user.language", "en");
preferredLanguage = Language.fromCode(property);
language = Language.fromCode(property);
// Make sure the user knows that we're using the system language for translations
LOGGER.info("Using system language ({}) for translations.", preferredLanguage);
LOGGER.info("Using system language ({}) for translations.", language);
} catch (Exception exception) {
LOGGER.warn("Could not use system language for translations.");
LOGGER.warn("Using default language ({}) for translations.", preferredLanguage);
LOGGER.warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", preferredLanguage);
LOGGER.info("Using custom language ({}) for translations.", language);
}
// Load all configurations
try {
generalConfig.load(getAsset("config", preferredLanguage));
messagesConfig.load(getAsset("messages", preferredLanguage));
webhookConfig.load(getAsset("webhook", preferredLanguage));
generalConfig.load(getAsset("config", language));
messagesConfig.load(getAsset("messages", language));
webhookConfig.load(getAsset("webhook", language));
} catch (Exception exception) {
throw new IllegalStateException("Error loading configuration", exception);
}
@@ -153,7 +158,7 @@ public final class SonarConfiguration {
// General settings
logPlayerAddresses = generalConfig.getBoolean("general.log-player-addresses");
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 1, 100);
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 0, 99);
// Attack tracker
minPlayersForAttack = clamp(generalConfig.getInt("attack-tracker.min-players-for-attack"), 2, 1024);
@@ -163,11 +168,6 @@ public final class SonarConfiguration {
// Database
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
database.host = generalConfig.getString("database.host");
database.port = generalConfig.getInt("database.port");
database.name = generalConfig.getString("database.name");
database.username = generalConfig.getString("database.username");
database.password = generalConfig.getString("database.password");
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age"), 1, 365);
// Queue
@@ -186,12 +186,11 @@ public final class SonarConfiguration {
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.collision.enabled");
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
verification.vehicle.enabled = generalConfig.getBoolean("verification.checks.vehicle.enabled");
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
@@ -224,6 +223,7 @@ public final class SonarConfiguration {
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
@@ -238,8 +238,6 @@ public final class SonarConfiguration {
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
verification.whitelistedProtocols.clear();
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols"));
verification.blacklistedProtocols.clear();
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols"));
@@ -421,25 +419,12 @@ public final class SonarConfiguration {
private boolean enabled;
private boolean checkCollisions;
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
// Keep this for backwards compatibility
SPECTATOR(2);
private final int id;
}
}
@Getter
public static final class Vehicle {
private Timing timing;
private boolean enabled;
}
@Getter
@@ -449,6 +434,23 @@ public final class SonarConfiguration {
private Pattern validRegex;
}
private Gamemode gamemode;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
NOT_SET(-1),
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2);
private final int id;
public boolean isSurvivalOrAdventure() {
return this == SURVIVAL || this == ADVENTURE;
}
}
private boolean checkGeyser;
private boolean logConnections;
private boolean logDuringAttack;
@@ -463,7 +465,6 @@ public final class SonarConfiguration {
private int rememberTime;
private int blacklistTime;
private int blacklistThreshold;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooFastReconnect;
@@ -484,20 +485,38 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
NONE(null, null);
MYSQL("MySQL", "jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter(),
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.4.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build()),
MARIADB("MariaDB", "jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter(),
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.4.0")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()),
H2("H2", "jdbc:h2:file:%s", new H2DatabaseTypeAdapter(),
Library.builder()
.groupId("com{}h2database")
.artifactId("h2")
.version("2.1.214")
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
.build()),
NONE("None", null, null, null);
private final String displayName;
private final String connectionString;
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
}
private Type type;
private String host;
private int port;
private String name;
private String username;
private String password;
private int maximumAge;
}
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
@@ -33,6 +34,7 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.io.File;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
@@ -70,7 +72,7 @@ public final class VerifiedPlayerController {
@Getter
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = database.getType();
@@ -81,16 +83,41 @@ public final class VerifiedPlayerController {
return;
}
// Initialize MySQL/MariaDB driver
database.getType().getDatabaseType().loadDriver();
// Make sure to only download the driver once
if (!cachedDatabaseType.isDownloaded()) {
LOGGER.info("Downloading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
// Download and load the driver for the current database
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName()),
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
final String jdbcURL;
// Create table
// H2 has a different JDBC URL layout
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
final File file = new File(Sonar.get().getConfig().getPluginFolder(),
Sonar.get().getConfig().getGeneralConfig().getString("database.filename"));
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
} else {
// Normal JDBC URL layout for MySQL/MariaDB/...
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
Sonar.get().getConfig().getGeneralConfig().getString("database.host"),
Sonar.get().getConfig().getGeneralConfig().getInt("database.port"),
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
// Create database table
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException exception) {
@@ -126,7 +153,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);
@@ -245,37 +271,44 @@ public final class VerifiedPlayerController {
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
/*
* TODO: Work on this
* return MAP.values().stream()
* .mapToInt(Collection::size)
* .sum();
*/
public int estimatedSize() {
return cache.size();
}
/**
* @return List of UUIDs associated with one IP address
* Returns the sum of verified IP addresses
* and their respective collection of UUIDs
*
* @return Exact size of all total verified players
*/
@Deprecated
public synchronized int exactSize() {
return cache.values().stream()
.mapToInt(Collection::size)
.sum();
}
/**
* @return {@link java.util.Collection} of UUIDs associated with an IP address
*/
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
* Clear the local cache, and then if the database type is set,
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
*/
public void clearAll() {
try {
cache.clear();
public synchronized void clearAll() {
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
// Delete the entire table from the database, if necessary
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
}
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.event.SonarEvent;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class AttackMitigatedEvent implements SonarEvent {
private final AttackTracker.AttackStatistics attackStatistics;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final @NotNull FallbackUser user;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -28,7 +27,6 @@ import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
@@ -75,10 +75,6 @@ public final class Fallback {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
}
public boolean shouldPerformVehicleCheck() {
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
}
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
@@ -18,8 +18,14 @@
package xyz.jonesdev.sonar.api.fallback;
public interface FallbackPipelines {
String FALLBACK_HANDLER = "sonar-fallback-handler";
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
String FALLBACK_INACTIVE_LISTENER = "sonar-inactive-listener";
String FALLBACK_INBOUND_HANDLER = "sonar-inbound-handler";
String FALLBACK_FRAME_DECODER = "sonar-frame-decoder";
String FALLBACK_FRAME_ENCODER = "sonar-frame-encoder";
String FALLBACK_TIMEOUT = "sonar-timeout";
String FALLBACK_PACKET_HANDLER = "sonar-packet-handler";
String FALLBACK_PACKET_ENCODER = "sonar-packet-encoder";
String FALLBACK_PACKET_DECODER = "sonar-packet-decoder";
String FALLBACK_TAIL_EXCEPTIONS = "sonar-exception-tail";
String FALLBACK_BANDWIDTH = "sonar-bandwidth-counter";
}
@@ -44,6 +44,9 @@ public interface FallbackUser {
@NotNull
SystemTimer getLoginTimer();
@NotNull
UUID getOfflineUuid();
boolean isReceivedClientSettings();
void setReceivedClientSettings(final boolean receivedClientSettings);
@@ -65,16 +68,10 @@ public interface FallbackUser {
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param handler Name of the main pipeline
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String handler);
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
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"};
}
}
@@ -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"};
@@ -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"};
+5 -6
View File
@@ -6,14 +6,13 @@ plugins {
bukkit {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
apiVersion = "1.13" // ignore legacy plugin warning
foliaSupported = true
commands {
@@ -33,11 +32,11 @@ dependencies {
implementation(project(":common"))
compileOnly(rootProject.libs.spigot)
testCompileOnly(rootProject.libs.spigot)
implementation(rootProject.libs.adventure.platform.bukkit)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.adventure.platform.bukkit) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bukkit)
implementation(rootProject.libs.libby.bukkit)
}
@@ -23,24 +23,24 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(),
new BukkitLibraryManager(plugin, plugin.getDataFolder().getName()));
INSTANCE = this;
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
}
/**
@@ -56,6 +56,11 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
return bukkitAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bukkitAudiences.sender((CommandSender) object);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -82,6 +87,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
private Metrics metrics;
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
@Override
public void enable() {
// Initialize bStats.org metrics
@@ -92,9 +99,23 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Try to inject into the server
if (FallbackBukkitInjector.isLateBindEnabled()) {
getPlugin().getServer().getScheduler().runTask(getPlugin(), FallbackBukkitInjector::inject);
} else {
getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin());
}
// Let the injector know that the plugin has been enabled
INITIALIZE_LISTENER.complete(null);
}
@Override
@@ -18,10 +18,19 @@
package xyz.jonesdev.sonar.bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
public final class SonarBukkitPlugin extends JavaPlugin {
private SonarBukkit bootstrap;
@Override
public void onLoad() {
// Inject early if late-bind is disabled
if (!FallbackBukkitInjector.isLateBindEnabled()) {
FallbackBukkitInjector.inject();
}
}
@Override
public void onEnable() {
bootstrap = new SonarBukkit(this);
@@ -23,9 +23,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import java.util.List;
@@ -41,7 +41,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import lombok.Getter;
@Getter
public enum BukkitServerVersion {
MINECRAFT_1_7_10,
MINECRAFT_1_8,
MINECRAFT_1_8_3,
MINECRAFT_1_8_8,
MINECRAFT_1_9,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_10_1,
MINECRAFT_1_10_2,
MINECRAFT_1_11,
MINECRAFT_1_11_2,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13,
MINECRAFT_1_13_1,
MINECRAFT_1_13_2,
MINECRAFT_1_14,
MINECRAFT_1_14_1,
MINECRAFT_1_14_2,
MINECRAFT_1_14_3,
MINECRAFT_1_14_4,
MINECRAFT_1_15,
MINECRAFT_1_15_1,
MINECRAFT_1_15_2,
MINECRAFT_1_16,
MINECRAFT_1_16_1,
MINECRAFT_1_16_2,
MINECRAFT_1_16_3,
MINECRAFT_1_16_4,
MINECRAFT_1_16_5,
MINECRAFT_1_17,
MINECRAFT_1_17_1,
MINECRAFT_1_18,
MINECRAFT_1_18_1,
MINECRAFT_1_18_2,
MINECRAFT_1_19,
MINECRAFT_1_19_1,
MINECRAFT_1_19_2,
MINECRAFT_1_19_3,
MINECRAFT_1_19_4,
MINECRAFT_1_20,
MINECRAFT_1_20_1,
MINECRAFT_1_20_2,
MINECRAFT_1_20_3,
MINECRAFT_1_20_4,
MINECRAFT_1_20_5,
MINECRAFT_1_20_6,
MINECRAFT_1_21;
static final BukkitServerVersion[] REVERSED_VALUES;
static {
REVERSED_VALUES = values().clone();
final int size = REVERSED_VALUES.length;
final int half = size / 2;
for (int i = 0; i < half; i++) {
final BukkitServerVersion temp = REVERSED_VALUES[i];
REVERSED_VALUES[i] = REVERSED_VALUES[size - 1 - i];
REVERSED_VALUES[size - 1 - i] = temp;
}
}
private final String release;
BukkitServerVersion() {
this.release = name().substring(10).replace("_", ".");
}
}
@@ -0,0 +1,250 @@
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.net.SocketAddress;
import java.util.HashSet;
import java.util.Set;
public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter {
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
handler.channelInactive(proxy);
} catch (Exception exception) {
handler.exceptionCaught(proxy, exception);
}
}
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class ProxiedChannelHandlerContext implements ChannelHandlerContext {
private final ChannelHandlerContext ctx;
// Overwrite to prevent call this method on next handler
@Override
public ChannelHandlerContext fireChannelInactive() {
return this;
}
@Override
public ChannelHandlerContext fireExceptionCaught(Throwable throwable) {
return this;
}
@Override
public Channel channel() {
return ctx.channel();
}
@Override
public EventExecutor executor() {
return ctx.executor();
}
@Override
public String name() {
return ctx.name();
}
@Override
public ChannelHandler handler() {
return ctx.handler();
}
@Override
public boolean isRemoved() {
return ctx.isRemoved();
}
@Override
public ChannelHandlerContext fireChannelRegistered() {
return ctx.fireChannelRegistered();
}
@Override
public ChannelHandlerContext fireChannelUnregistered() {
return ctx.fireChannelUnregistered();
}
@Override
public ChannelHandlerContext fireChannelActive() {
return ctx.fireChannelActive();
}
@Override
public ChannelHandlerContext fireUserEventTriggered(Object o) {
return ctx.fireUserEventTriggered(o);
}
@Override
public ChannelHandlerContext fireChannelRead(Object o) {
return ctx.fireChannelRead(o);
}
@Override
public ChannelHandlerContext fireChannelReadComplete() {
return ctx.fireChannelReadComplete();
}
@Override
public ChannelHandlerContext fireChannelWritabilityChanged() {
return ctx.fireChannelWritabilityChanged();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress) {
return ctx.bind(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress) {
return ctx.connect(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
return ctx.connect(socketAddress, socketAddress1);
}
@Override
public ChannelFuture disconnect() {
return ctx.disconnect();
}
@Override
public ChannelFuture close() {
return ctx.close();
}
@Override
public ChannelFuture deregister() {
return ctx.deregister();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.bind(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, socketAddress1, channelPromise);
}
@Override
public ChannelFuture disconnect(ChannelPromise channelPromise) {
return ctx.disconnect(channelPromise);
}
@Override
public ChannelFuture close(ChannelPromise channelPromise) {
return ctx.close(channelPromise);
}
@Override
public ChannelFuture deregister(ChannelPromise channelPromise) {
return ctx.deregister(channelPromise);
}
@Override
public ChannelHandlerContext read() {
return ctx.read();
}
@Override
public ChannelFuture write(Object o) {
return ctx.write(o);
}
@Override
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
return ctx.write(o, channelPromise);
}
@Override
public ChannelHandlerContext flush() {
return ctx.flush();
}
@Override
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
return ctx.writeAndFlush(o, channelPromise);
}
@Override
public ChannelFuture writeAndFlush(Object o) {
return ctx.writeAndFlush(o);
}
@Override
public ChannelPromise newPromise() {
return ctx.newPromise();
}
@Override
public ChannelProgressivePromise newProgressivePromise() {
return ctx.newProgressivePromise();
}
@Override
public ChannelFuture newSucceededFuture() {
return ctx.newSucceededFuture();
}
@Override
public ChannelFuture newFailedFuture(Throwable throwable) {
return ctx.newFailedFuture(throwable);
}
@Override
public ChannelPromise voidPromise() {
return ctx.voidPromise();
}
@Override
public ChannelPipeline pipeline() {
return ctx.pipeline();
}
@Override
public ByteBufAllocator alloc() {
return ctx.alloc();
}
@Override
@Deprecated
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
return ctx.attr(attributeKey);
}
@Override
@Deprecated
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
return ctx.hasAttr(attributeKey);
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
FallbackBukkitInboundHandler() {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
if (inactiveListener != null) {
inactiveListener.add(handler);
}
};
}
private FallbackPacketRegistry.ProtocolRegistry registry;
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
final @NotNull ProtocolVersion protocolVersion) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
// Don't continue if the ByteBuf isn't readable or the channel is closed
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
// Let the actual handler know about the handshake packet
handleHandshake(ctx.channel(), handshake.getHostname(), handshake.getProtocolVersionId());
// Be ready for the next packet (which is supposed to be a login packet)
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf);
}
}
}
@@ -0,0 +1,272 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
// https://github.com/dmulloy2/ProtocolLib/blob/master/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/handlers/BukkitChannelInitializer.java
@UtilityClass
public class FallbackBukkitInjector {
private final String LEGACY_NMS_PACKAGE;
private final String OBC_PACKAGE;
private final BukkitServerVersion SERVER_VERSION;
private final boolean USES_LEGACY_PACKAGING, OBFUSCATED;
private final Class<?> MINECRAFT_SERVER_CLASS;
private final Class<?> CRAFTBUKKIT_SERVER_CLASS;
private final Class<?> SERVER_CONNECTION_CLASS;
private final Object SERVER_INSTANCE;
@Getter
private boolean lateBindEnabled;
static {
SERVER_VERSION = resolveServerVersion();
USES_LEGACY_PACKAGING = SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_17) < 0;
try {
final String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
String modifiedPackageName;
if (SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_20_5) >= 0) {
// Example: org.bukkit.craftbukkit
modifiedPackageName = bukkitPackage.split("\\.")[2];
} else {
// Example: org.bukkit.craftbukkit.v1_16_R3
modifiedPackageName = bukkitPackage.split("\\.")[3];
}
LEGACY_NMS_PACKAGE = "net.minecraft.server." + modifiedPackageName + ".";
OBC_PACKAGE = bukkitPackage + ".";
OBFUSCATED = isObfuscated();
// Minecraft classes
MINECRAFT_SERVER_CLASS = getNMSClass("server.MinecraftServer", "MinecraftServer");
SERVER_CONNECTION_CLASS = getNMSClass(OBFUSCATED ? "server.network.ServerConnection" : "server.network.ServerConnectionListener", "ServerConnection");
// CraftBukkit classes
CRAFTBUKKIT_SERVER_CLASS = getOBCClass("CraftServer");
Object minecraftServerInstance;
try {
// 1.20.5+
minecraftServerInstance = getFieldAt(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(null);
} catch (Exception exception) {
minecraftServerInstance = getFieldAt(CRAFTBUKKIT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(Bukkit.getServer());
}
SERVER_INSTANCE = minecraftServerInstance;
checkLateBind();
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
private void checkLateBind() {
try {
try {
Class.forName("org.bukkit.event.server.ServerLoadEvent");
// late-bind does not exist on these versions, ignore this case
return;
} catch (ClassNotFoundException ignored) {
}
final Class<?> spigotConfiguration = Class.forName("org.spigotmc.SpigotConfig");
final Method initFieldMethod = spigotConfiguration.getDeclaredMethod("lateBind");
initFieldMethod.setAccessible(true);
initFieldMethod.invoke(null);
if ((boolean) spigotConfiguration.getField("lateBind").get(null)) {
lateBindEnabled = true;
}
} catch (java.lang.ReflectiveOperationException ignore) {
}
}
public @NotNull Field getFieldAt(final @NotNull Class<?> clazz,
final @NotNull Class<?> type,
final int index) {
int currentIndex = 0;
for (final Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (type.isAssignableFrom(field.getType())) {
if (currentIndex++ == index) {
return field;
}
}
}
if (clazz.getSuperclass() != null) {
return getFieldAt(clazz.getSuperclass(), type, index);
}
throw new IllegalStateException("Could not find field #" + index + " in " + clazz.getName());
}
public static @NotNull Field findField(final boolean checkSuperClass,
final @NotNull Class<?> clazz,
final String @NotNull ... types) throws NoSuchFieldException {
for (final Field field : clazz.getDeclaredFields()) {
final String fieldTypeName = field.getType().getSimpleName();
for (final String type : types) {
if (fieldTypeName.equals(type)) {
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
return field;
}
}
}
if (checkSuperClass && clazz != Object.class && clazz.getSuperclass() != null) {
return findField(true, clazz.getSuperclass(), types);
}
throw new NoSuchFieldException(types[0]);
}
private @NotNull BukkitServerVersion resolveServerVersion() {
final String bukkitVersion = Bukkit.getBukkitVersion();
for (final BukkitServerVersion serverVersion : BukkitServerVersion.REVERSED_VALUES) {
if (bukkitVersion.contains(serverVersion.getRelease())) {
return serverVersion;
}
}
// Throw an error if we cannot find the server version
throw new IllegalStateException("Could not find server version " + bukkitVersion);
}
private boolean isObfuscated() {
try {
Class.forName("net.minecraft.server.network.PlayerConnection");
return true;
} catch (ClassNotFoundException exception) {
return false;
}
}
private @NotNull Class<?> getNMSClass(final String modern, final String legacy) throws ClassNotFoundException {
return Class.forName(USES_LEGACY_PACKAGING ? LEGACY_NMS_PACKAGE + legacy : "net.minecraft." + modern);
}
public @NotNull Class<?> getOBCClass(final String clazz) throws ClassNotFoundException {
return Class.forName(OBC_PACKAGE + clazz);
}
@SuppressWarnings("unchecked")
public void inject() {
try {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
final var list = (List<?>) field.get(getFieldAt(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(SERVER_INSTANCE));
for (final Object object : list) {
final ChannelFuture channelFuture = (ChannelFuture) object;
final List<String> names = channelFuture.channel().pipeline().names();
ChannelHandler bootstrap = null;
for (final String name : names) {
try {
final ChannelHandler handler = channelFuture.channel().pipeline().get(name);
final Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var childHandler = (ChannelInitializer<?>) childHandlerField.get(handler);
if (childHandler != null) {
bootstrap = handler;
}
break;
} catch (Exception exception) {
// Some plugins modify this, so we better skip any exceptions
}
}
if (bootstrap == null) {
// Default to the first channel future if we couldn't find a childHandler
bootstrap = channelFuture.channel().pipeline().first();
}
final Field childHandlerField = bootstrap.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
throw new ReflectiveOperationException(exception);
}
});
}
return;
}
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.listener;
import io.netty.channel.Channel;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle handleMethod;
// ServerGamePacketListenerImpl aka. PlayerConnection
private static MethodHandle packetListenerField;
private static MethodHandle managerField;
private static MethodHandle channelField;
private static boolean initialized;
static {
try {
final Method handleMethod = FallbackBukkitInjector.getOBCClass("entity.CraftPlayer")
.getDeclaredMethod("getHandle");
final Field listenerField = FallbackBukkitInjector.findField(false, handleMethod.getReturnType(),
"PlayerConnection", "ServerGamePacketListenerImpl");
final Field managerField = FallbackBukkitInjector.findField(true, listenerField.getType(),
"NetworkManager", "Connection");
final Field channelField = FallbackBukkitInjector.getFieldAt(managerField.getType(), Channel.class, 0);
BukkitJoinListener.handleMethod = MethodHandles.lookup().unreflect(handleMethod);
packetListenerField = MethodHandles.lookup().unreflectGetter(listenerField);
BukkitJoinListener.managerField = MethodHandles.lookup().unreflectGetter(managerField);
BukkitJoinListener.channelField = MethodHandles.lookup().unreflectGetter(channelField);
initialized = true;
} catch (Exception exception) {
initialized = false;
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
if (!initialized || event.getPlayer().hasMetadata("NPC")) {
return;
}
try {
final Object handle = handleMethod.invoke(event.getPlayer());
final Object listener = packetListenerField.invoke(handle);
final Object networkManager = managerField.invoke(listener);
final Channel channel = (Channel) channelField.invoke(networkManager);
// Close the channel if Sonar's handlers are not found when the PlayerJoinEvent is called
if (channel != null && channel.isActive() && channel.pipeline().context(FALLBACK_BANDWIDTH) == null) {
channel.close();
}
} catch (Throwable throwable) {
Sonar.get().getLogger().warn("Failed to the channel for {}: {}", event.getPlayer().getName(), throwable);
}
}
}
+3 -6
View File
@@ -4,8 +4,6 @@ plugins {
bungee {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
@@ -18,10 +16,9 @@ dependencies {
compileOnly(rootProject.libs.bungeecord)
testCompileOnly(rootProject.libs.bungeecord)
implementation(rootProject.libs.adventure.platform.bungee)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.adventure.platform.bungee) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
@@ -21,6 +21,7 @@ import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
@@ -28,18 +29,15 @@ 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;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
INSTANCE = this;
}
/**
@@ -55,6 +53,11 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
return bungeeAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bungeeAudiences.sender((CommandSender) object);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -91,12 +94,16 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
FallbackBungeeInjector.inject();
}
@Override
@@ -22,9 +22,9 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import static java.util.Collections.emptyList;
@@ -38,7 +38,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -27,19 +27,13 @@ import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
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 FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
@@ -53,17 +47,22 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (listenForPackets && msg instanceof PacketWrapper) {
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
}
}
// Intercept any server login packet by the client
else if (wrappedPacket instanceof LoginRequest) {
@@ -77,9 +76,12 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
throw new ReflectiveOperationException(throwable);
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
handleLogin(channel, ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
return;
}
}
@@ -22,12 +22,16 @@ import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import net.md_5.bungee.netty.PipelineUtils;
import sun.misc.Unsafe;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackInjectionHelper {
public class FallbackBungeeInjector {
public void inject() {
try {
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
@@ -35,7 +39,9 @@ public class FallbackInjectionHelper {
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackBungeeInboundHandler()));
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
@@ -47,9 +53,9 @@ public class FallbackInjectionHelper {
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer
unsafe.putObject(base, offset, fallbackInitializer);
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -56,20 +56,6 @@ class LibraryLoader {
.artifactId("ormlite-jdbc")
.version("6.1")
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
.build(),
// MySQL driver
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.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")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()
);
@@ -87,6 +73,12 @@ class LibraryLoader {
.artifactId("adventure-text-serializer-gson")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-nbt")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build()
);
}
@@ -41,6 +41,7 @@ import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
import java.time.Duration;
@@ -55,6 +56,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
private Notification notificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private final LibraryManager libraryManager;
private final SonarStatistics statistics;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
@@ -66,6 +68,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
final @NotNull LibraryManager libraryManager) {
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager, platform);
this.libraryManager = libraryManager;
// Check if Netty is up-to-date
ProtocolUtil.checkNettyVersion();
// Set the Sonar API
SonarSupplier.set(this);
// Set the plugin instance
@@ -180,7 +185,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
verifiedPlayerController = new VerifiedPlayerController(libraryManager);
}
public final void shutdown() {
@@ -1,247 +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.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserDetection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
protected @Nullable String username;
protected InetAddress inetAddress;
protected ProtocolVersion protocolVersion;
protected boolean listenForPackets = true;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public final void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
FALLBACK.getConnected().compute(inetAddress, (key, value) -> null);
// Remove the IP address from the queue
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();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull String hostname, final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress,
final @NotNull String encoder,
final @NotNull String decoder,
final @NotNull String timeout,
final @NotNull String handler) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
this.inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, blacklisted, encoder, handler);
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);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserDetection.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, loginPacket, 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())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted, encoder, handler);
return;
}
// Make sure we actually have to verify the player
final String offlineUUIDString = "OfflinePlayer:" + username;
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString(), offlineUUID)) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast, encoder, handler);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Queue the connection for further processing
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()
.matcher(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername, encoder, handler);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
});
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
*/
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String encoder,
final @NotNull String handler) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null ? 1 : value + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(channel, protocolVersion, tooManyOnlinePerIP, encoder, handler);
return;
}
}
// Don't listen for further packets
// TODO: deject the pipeline without breaking disconnect logic
listenForPackets = false;
// Let the server know about the login packet
ctx.fireChannelRead(loginPacket);
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import java.util.function.Consumer;
@RequiredArgsConstructor
public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
private final Consumer<ChannelPipeline> sonarPipelineInjector;
@Setter
@Getter
private InetAddress inetAddress;
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
// Remove the IP address from the queue
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,293 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.*;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@AllArgsConstructor
@RequiredArgsConstructor
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
protected @Nullable String username;
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull Channel channel,
final @NotNull String hostname,
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param channel Forwarded client channel
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull Channel channel,
final @NotNull ChannelHandlerContext ctx,
final @NotNull Runnable loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, alreadyVerifying);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Make sure we actually have to verify the player
final String offlineUuidString = "OfflinePlayer:" + username;
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inboundHandler.getInetAddress().toString(), offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
// Remove all other pipelines that could still mess up something
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
// Remove other instances of this IP address from the queue
return null;
}
return () -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUuid);
};
});
}
/**
* Removes all pipelines and rewrites them using our custom handlers
*/
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
final @NotNull RemovalListener removalListener) {
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
// Don't accidentally remove Sonar's handlers
if (entry.getKey().startsWith("sonar")
// Don't remove floodgate's pipelines
|| entry.getKey().startsWith("geyser")
|| entry.getKey().startsWith("floodgate")
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
continue;
}
ctx.pipeline().remove(entry.getValue());
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
}
// Add our custom pipelines
ctx.pipeline()
.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarInt21FrameEncoder.INSTANCE)
.addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
Sonar.get().getConfig().getVerification().getReadTimeout(),
Sonar.get().getConfig().getVerification().getWriteTimeout(),
TimeUnit.MILLISECONDS))
.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded client channel context
* @param inetAddress Resolved client IP address
* @param loginPacket Login packet sent by the client
*/
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
}
}
// Let the server know about the login packet
loginPacket.run();
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
private void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
}
}
private void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
// Remove the main pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
@FunctionalInterface
public interface RemovalListener {
void accept(final @NotNull ChannelPipeline pipeline,
final @NotNull String name,
final @NotNull ChannelHandler handler);
RemovalListener EMPTY = (pipeline, name, handler) -> {};
}
}
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
@@ -34,7 +33,7 @@ import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
@@ -46,9 +45,8 @@ import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
@@ -59,6 +57,7 @@ public final class FallbackUserWrapper implements FallbackUser {
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final UUID offlineUuid;
@Setter
private boolean receivedClientSettings;
@Setter
@@ -69,11 +68,13 @@ public final class FallbackUserWrapper implements FallbackUser {
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull UUID offlineUuid,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.offlineUuid = offlineUuid;
this.geyser = geyser;
}
@@ -85,9 +86,7 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String handler) {
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
// The player has joined the verification
GlobalSonarStatistics.totalAttemptedVerifications++;
@@ -107,37 +106,34 @@ public final class FallbackUserWrapper implements FallbackUser {
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (key, value) -> (byte) 0);
// Run this in the channel's event loop to avoid issues
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));
// Make sure the channel is still active
if (!channel.isActive()) {
return;
}
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
// 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);
}
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(FallbackPreparer.LOGIN_SUCCESS);
write(FallbackPreparer.loginSuccess);
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
// Listen for all incoming packets by setting the packet listener
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
// Make sure to catch all exceptions during the verification
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
});
}
@@ -212,8 +208,8 @@ public final class FallbackUserWrapper implements FallbackUser {
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
@@ -225,49 +221,4 @@ public final class FallbackUserWrapper implements FallbackUser {
}
}
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param encoder Encoder to replace
* @param handler Main pipeline to remove
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
public static void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String handler) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet, encoder, handler);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet, encoder, handler));
}
}
private static void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove the main pipeline to completely take over the channel
if (channel.pipeline().context(boss) != null) {
channel.pipeline().remove(boss);
}
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
}
@@ -15,22 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.Consumer;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
@RequiredArgsConstructor
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
public final class FallbackInjectedChannelInitializer extends ChannelInitializer<Channel> {
private static final MethodHandle INIT_CHANNEL_METHOD;
static {
@@ -42,7 +48,8 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
private final @NotNull ChannelInitializer<Channel> originalChannelInitializer;
private final @NotNull Consumer<ChannelPipeline> sonarPipelineInjector;
@Override
protected void initChannel(final Channel channel) throws Exception {
@@ -53,8 +60,16 @@ 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()) {
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}
}
}
@@ -0,0 +1,38 @@
/*
* 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.netty;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
@ChannelHandler.Sharable
public final class FallbackTailExceptionsHandler extends ChannelDuplexHandler {
public static final FallbackTailExceptionsHandler INSTANCE = new FallbackTailExceptionsHandler();
// We can override the default exceptionCaught method since the server
// does not have any other pipelines that could handle this error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,57 @@
/*
* 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.netty;
import io.netty.util.ByteProcessor;
import lombok.Getter;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
@Getter
final class FallbackVarInt21ByteDecoder implements ByteProcessor {
private int readVarInt, bytesRead;
private DecodeResult result = DecodeResult.TOO_SHORT;
@Override
public boolean process(byte k) {
if (k == 0 && bytesRead == 0) {
result = DecodeResult.RUN_OF_ZEROES;
return true;
}
if (result == DecodeResult.RUN_OF_ZEROES) {
return false;
}
readVarInt |= (k & 0x7F) << bytesRead++ * 7;
if (bytesRead > 3) {
result = DecodeResult.TOO_BIG;
return false;
}
if ((k & 0x80) != 128) {
result = DecodeResult.SUCCESS;
return false;
}
return true;
}
public enum DecodeResult {
SUCCESS,
TOO_SHORT,
TOO_BIG,
RUN_OF_ZEROES
}
}
@@ -0,0 +1,77 @@
/*
* 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.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
if (!ctx.channel().isActive()) {
in.clear();
return;
}
final FallbackVarInt21ByteDecoder reader = new FallbackVarInt21ByteDecoder();
final int end = in.forEachByte(reader);
if (end == -1) {
// We tried to go beyond the end of the buffer.
// This is probably a good sign that the buffer was too short to hold a proper varint.
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// Special case where the entire packet is just a run of zeroes. We ignore them all.
in.clear();
}
return;
}
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// This will return to the point where the next varint starts
in.readerIndex(end);
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.SUCCESS) {
final int readVarInt = reader.getReadVarInt();
final int bytesRead = reader.getBytesRead();
if (readVarInt < 0) {
in.clear();
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
} else if (readVarInt == 0) {
// Skip over the empty packet(s) and ignore it
in.readerIndex(end + 1);
} else {
int minimumRead = bytesRead + readVarInt;
if (in.isReadable(minimumRead)) {
out.add(in.retainedSlice(end + 1, readVarInt));
in.skipBytes(minimumRead);
}
}
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
in.clear();
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
}
}
}
@@ -0,0 +1,50 @@
/*
* 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.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
@Override
protected void encode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final @NotNull ByteBuf out) throws Exception {
writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
@Override
protected ByteBuf allocateBuffer(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
final int anticipatedRequiredCapacity = varIntBytes(msg.readableBytes()) + msg.readableBytes();
return ctx.alloc().buffer(anticipatedRequiredCapacity);
}
}
@@ -26,7 +26,9 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -38,6 +40,12 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
HANDSHAKE {
{
serverbound.register(HandshakePacket.class, HandshakePacket::new,
map(0x00, MINECRAFT_1_7_2, false));
}
},
LOGIN {
{
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
@@ -45,6 +53,8 @@ public enum FallbackPacketRegistry {
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, true));
serverbound.register(LoginStartPacket.class, LoginStartPacket::new,
map(0x00, MINECRAFT_1_7_2, false));
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
}
@@ -124,7 +134,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(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
clientbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
map(0x2F, MINECRAFT_1_12_1, true),
@@ -444,7 +454,7 @@ public enum FallbackPacketRegistry {
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false),
map(0x12, MINECRAFT_1_21, false));
serverbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
serverbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
map(0x0F, MINECRAFT_1_12, false),
@@ -26,20 +26,21 @@ import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
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.security.SecureRandom;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@UtilityClass
public class FallbackPreparer {
private final SecureRandom RANDOM = new SecureRandom();
// LoginSuccess
public final FallbackPacket LOGIN_SUCCESS = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L), "Sonar"));
public FallbackPacket loginSuccess;
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
@@ -60,7 +61,7 @@ public class FallbackPreparer {
public FallbackPacket enterCodeMessage;
public FallbackPacket incorrectCaptcha;
// JoinGame
public final int PLAYER_ENTITY_ID = ThreadLocalRandom.current().nextInt(10);
public final int PLAYER_ENTITY_ID = RANDOM.nextInt(10);
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
@@ -70,7 +71,7 @@ public class FallbackPreparer {
// Default Spawn Position
public FallbackPacket defaultSpawnPosition;
// Spawn Position
public final int TELEPORT_ID = ThreadLocalRandom.current().nextInt();
public final int TELEPORT_ID = RANDOM.nextInt();
public FallbackPacket spawnPosition;
// Transfer packet
public static FallbackPacket transferToOrigin;
@@ -86,21 +87,23 @@ public class FallbackPreparer {
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket spawnEntity;
public static FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public final int IN_AIR_Y_POSITION = 1337;
// Captcha position
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false));
// CAPTCHA position
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, false));
// Platform
public BlockType blockType = BlockType.BARRIER;
public BlockType blockType;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
@@ -117,9 +120,13 @@ public class FallbackPreparer {
Sonar.get().getLogger().info("Preloading all registered packets...");
FallbackPacketRegistry.values();
// Prepare LoginSuccess packet
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L),
Sonar.get().getConfig().getGeneralConfig().getString("verification.cached-username")));
// Prepare JoinGame packet
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
Sonar.get().getConfig().getVerification().getGamemode().getId(),
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
@@ -139,11 +146,13 @@ public class FallbackPreparer {
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0, -90, TELEPORT_ID, false));
// Prepare collision platform positions
blockType = BlockType.valueOf(Sonar.get().getConfig().getGeneralConfig().getString(
"verification.checks.collision.collision-block-type").toUpperCase());
final BlockUpdate[] changedBlocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
int index = 0;
@@ -180,6 +189,8 @@ public class FallbackPreparer {
// Prepare packets for the vehicle check
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
spawnEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
@@ -27,12 +27,111 @@ import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE(protocolVersion -> 1, 1D),
STONE(protocolVersion -> 1, 1),
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
ENCHANTMENT_TABLE(protocolVersion -> {
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
default:
// 1.7.2-1.12.2
return 116;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 4612;
case MINECRAFT_1_13_2:
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
return 4613;
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 5116;
case MINECRAFT_1_16_1:
return 5132;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 5136;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 5333;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 5719;
case MINECRAFT_1_19_3:
return 7159;
case MINECRAFT_1_19_4:
return 7385;
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 7389;
}
}, 0.75f),
TRAPDOOR(protocolVersion -> {
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
return 96;
default:
// 1.8-1.12.2
return 167;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6509;
case MINECRAFT_1_13_2:
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
return 6510;
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 7016;
case MINECRAFT_1_16_1:
return 7552;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7556;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7802;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8293;
case MINECRAFT_1_19_3:
return 9937;
case MINECRAFT_1_19_4:
return 10269;
case MINECRAFT_1_20:
return 10273;
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10414;
}
}, 0.1875),
BARRIER(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
@@ -82,7 +181,7 @@ public enum BlockType {
case MINECRAFT_1_21:
return 10366;
}
}, 1D);
}, 1);
private final Function<ProtocolVersion, Integer> idFunction;
// TODO: Implement per-version block heights?
@@ -0,0 +1,57 @@
/*
* 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.handshake;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@Getter
@ToString(of = {"protocolVersionId", "hostname", "port"})
public final class HandshakePacket implements FallbackPacket {
private int protocolVersionId;
private String hostname;
private int port;
private int intent;
private static final String FORGE_TOKEN = "\0FML\0";
private static final int MAXIMUM_HOSTNAME_LENGTH = 255 + FORGE_TOKEN.length() + 1;
// https://wiki.vg/Protocol#Handshaking
public static final int STATUS = 1;
public static final int LOGIN = 2;
public static final int TRANSFER = 3;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
protocolVersionId = readVarInt(byteBuf);
hostname = readString(byteBuf, MAXIMUM_HOSTNAME_LENGTH);
port = byteBuf.readUnsignedShort();
intent = readVarInt(byteBuf);
}
}
@@ -0,0 +1,76 @@
/*
* 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.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class LoginStartPacket implements FallbackPacket {
private String username;
private @Nullable UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
username = readString(byteBuf, 16);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) {
if (byteBuf.readBoolean()) {
readPlayerKey(byteBuf);
}
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
uuid = readUUID(byteBuf);
return;
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
if (byteBuf.readBoolean()) {
uuid = readUUID(byteBuf);
}
}
}
}
private static void readPlayerKey(final @NotNull ByteBuf byteBuf) {
final long expiry = byteBuf.readLong();
final byte[] key = readByteArray(byteBuf);
final byte[] signature = readByteArray(byteBuf, 4096);
}
}
@@ -34,7 +34,7 @@ public final class PaddleBoatPacket implements FallbackPacket {
private boolean leftPaddle, rightPaddle;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetPlayerPositionRotation implements FallbackPacket {
public final class SetPlayerPositionRotationPacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
private int teleportId;
@@ -46,15 +46,10 @@ public final class SetPlayerPositionRotation implements FallbackPacket {
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
byteBuf.writeBoolean(onGround);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeBoolean(onGround);
} else {
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, teleportId);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
@@ -25,6 +25,7 @@ import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import java.util.UUID;
@@ -37,23 +38,24 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@NoArgsConstructor
@AllArgsConstructor
public final class SpawnEntityPacket implements FallbackPacket {
private int entityId, entityType;
private int entityId;
private EntityType entityType;
private double x, y, z;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
writeVarInt(byteBuf, entityId);
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_8) > 0;
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_9) >= 0;
if (v1_9orHigher) {
writeUUID(byteBuf, UUID.randomUUID());
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, entityType);
writeVarInt(byteBuf, entityType.getId(protocolVersion));
} else {
byteBuf.writeByte(entityType);
byteBuf.writeByte(entityType.getId(protocolVersion));
}
if (v1_9orHigher) {
@@ -61,9 +63,9 @@ public final class SpawnEntityPacket implements FallbackPacket {
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
} else {
byteBuf.writeInt(floor(x * 32D));
byteBuf.writeInt(floor(y * 32D));
byteBuf.writeInt(floor(z * 32D));
byteBuf.writeInt((int) (x * 32D));
byteBuf.writeInt((int) (y * 32D));
byteBuf.writeInt((int) (z * 32D));
}
byteBuf.writeByte(0); // pitch or yaw
@@ -83,11 +85,6 @@ public final class SpawnEntityPacket implements FallbackPacket {
}
}
private static int floor(final double value) {
final int __value = (int) value;
return value < (double) __value ? __value - 1 : __value;
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.common.fallback.session;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@@ -25,8 +26,6 @@ import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.UUID;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
/**
@@ -44,13 +43,20 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
*/
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
public FallbackCAPTCHASessionHandler(final FallbackUser user, final String username, final UUID uuid) {
super(user, username, uuid);
public FallbackCAPTCHASessionHandler(final @NotNull FallbackUser user,
final @NotNull String username) {
super(user, username);
// Disconnect the player if there is no CAPTCHA available at the moment
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
throw new CorruptedFrameException("No captcha available");
}
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// 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.
// 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
@@ -61,7 +67,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
// Make sure the player knows what to do
// Make sure the player knows that they have to enter the code in chat
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
user.getChannel().flush();
@@ -72,34 +78,34 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
@Override
public void handle(final @NotNull FallbackPacket packet) {
// Check if the player took too long to enter the captcha
// Check if the player took too long to enter the CAPTCHA
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkState(!user.getLoginTimer().elapsed(maxDuration), "took too long to enter captcha");
checkState(!user.getLoginTimer().elapsed(maxDuration), "took too long to enter CAPTCHA");
// Handle incoming chat messages
if (packet instanceof SystemChatPacket) {
final SystemChatPacket chat = (SystemChatPacket) packet;
// Captcha is correct, finish verification
// Finish the verification if the player entered the correct code
if (chat.getMessage().equals(answer)) {
finishVerification();
return;
}
// Captcha is incorrect, remove one try
checkState(tries-- > 0, "failed captcha too often");
// Decrement the number of tries left
checkState(tries-- > 0, "failed CAPTCHA too often");
// Send the player a chat message to let them know that the code they entered is incorrect
user.write(incorrectCaptcha);
} else if (packet instanceof SetPlayerPositionPacket
|| packet instanceof SetPlayerPositionRotation) {
|| packet instanceof SetPlayerPositionRotationPacket) {
// A position packet is sent approximately every second
final long difference = maxDuration - user.getLoginTimer().delay();
final int index = (int) (difference / 1000D);
// Make sure we can actually safely get and send the packet
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
// Send the countdown using the experience bar
user.write(xpCountdown[index]);
if (Sonar.get().getConfig().getVerification().getGamemode().isSurvivalOrAdventure()) {
final long difference = maxDuration - user.getLoginTimer().delay();
final int index = (int) (difference / 1000D);
// Make sure we can actually safely get and send the packet
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
// Send the countdown using the experience bar
user.write(xpCountdown[index]);
}
lastCountdownIndex = index;
}
lastCountdownIndex = index;
// Send a KeepAlive packet every few seconds
if (keepAliveStreak++ > 20) {
keepAliveStreak = 0;
@@ -24,18 +24,15 @@ 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.*;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackGravitySessionHandler extends FallbackSessionHandler {
public FallbackGravitySessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid) {
super(user, username, uuid);
final @NotNull String username) {
super(user, username);
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
this.enableGravityCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
@@ -46,7 +43,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
user.delayedWrite(joinGame);
// Then, write the ClientAbilities packet to the buffer
// This is only necessary if the player is in creative mode
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
if (Sonar.get().getConfig().getVerification().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Write the PositionLook packet to the buffer
@@ -102,15 +99,15 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
// Send the player to the protocol check
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(new FallbackProtocolSessionHandler(user, username, uuid, forceCAPTCHA));
decoder.setListener(new FallbackProtocolSessionHandler(user, username, forceCAPTCHA));
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof SetPlayerPositionRotation) {
if (packet instanceof SetPlayerPositionRotationPacket) {
// Make sure the player has teleported before checking for position packets
if (teleported) {
final SetPlayerPositionRotation positionLook = (SetPlayerPositionRotation) packet;
final SetPlayerPositionRotationPacket positionLook = (SetPlayerPositionRotationPacket) packet;
handleMovement(positionLook.getX(), positionLook.getY(), positionLook.getZ(), positionLook.isOnGround());
}
} else if (packet instanceof SetPlayerPositionPacket) {
@@ -191,6 +188,10 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
movementTick++;
if (enableGravityCheck) {
// Ensure that the player is above the collision platform
checkState(y > DEFAULT_Y_COLLIDE_POSITION,
"fell through blocks; y: " + y + " deltaY: " + deltaY + " tick: " + movementTick);
// Predict the player's current motion based on the last motion
// https://minecraft.wiki/w/Entity#Motion_of_entities
final double predicted = (lastDeltaY - 0.08) * 0.98f;
@@ -220,7 +221,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
markSuccess(true);
return;
}
user.fail("illegal collision tick: " + movementTick);
user.fail("illegal collision tick; 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;
@@ -230,7 +231,14 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
collisionOffsetY += 1.62f;
}
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkState(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
if (collisionOffsetY < -0.03) {
// 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; offset: " + collisionOffsetY + " tick: " + movementTick + " y: " + y);
}
// The player has collided with the blocks, go on to the next stage
markSuccess(false);
}
@@ -29,7 +29,6 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformatio
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.KeepAlivePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@@ -61,9 +60,8 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid) {
super(user, username, uuid);
final @NotNull String username) {
super(user, username);
// Start initializing the actual join process for pre-1.20.2 clients
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
@@ -95,7 +93,7 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
// Make sure we can actually switch over to the next check
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
// Pass the player to the next verification handler
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username, uuid);
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username);
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(gravitySessionHandler);
}
@@ -25,15 +25,12 @@ 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);
super(user, username);
this.forceCAPTCHA = forceCAPTCHA;
@@ -80,10 +77,10 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
// 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));
if (Sonar.get().getConfig().getVerification().getVehicle().isEnabled()) {
decoder.setListener(new FallbackVehicleSessionHandler(user, username, forceCAPTCHA));
} else if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
} else {
// The player has passed all checks
finishVerification();
@@ -30,7 +30,6 @@ import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
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.*;
@@ -43,7 +42,6 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
public abstract class FallbackSessionHandler implements FallbackPacketListener {
protected final FallbackUser user;
protected final String username;
protected final UUID uuid;
protected static final Random RANDOM = new Random();
@@ -61,18 +59,22 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
// Add verified player to the database
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
user.getInetAddress(), uuid, user.getLoginTimer().getStart()));
user.getInetAddress(), user.getOfflineUuid(), user.getLoginTimer().getStart()));
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
username, uuid, user, user.getLoginTimer().delay()));
username, user.getOfflineUuid(), 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 (transferToOrigin != null
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
// Send the transfer packet to the player and close the channel
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
if (user.isGeyser()) {
user.write(transferToOrigin);
} else {
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
}
} else {
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
@@ -121,22 +123,22 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
}
protected final void validateClientBrand(final byte @NotNull [] data) {
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs invalid string checks by default.
String read = new String(data, StandardCharsets.UTF_8);
checkState(read.length() > 1, "client brand is too short");
// 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(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) {
read = read.substring(1);
brand = brand.substring(1);
}
// 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());
// 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) {
@@ -22,14 +22,9 @@ 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.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PaddleBoatPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PlayerInputPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPassengersPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SpawnEntityPacket;
import java.util.UUID;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@@ -40,54 +35,83 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
* {@link SpawnEntityPacket} and {@link SetPassengersPacket} packets are sent to the client,
* therefore, making the player enter a boat.
* <br>
* The boat will be spawned at y 1337.
* <br>
* After 3 {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets,
* the boat is removed.
* <br>
* Now we can check if the player sends the correct position after the entity is removed.
* Please note that we don't tell the client to exit the vehicle, therefore, possibly confusing some bots.
* Removing the boat results in the player exiting the vehicle on vanilla clients.
* <br>
* See more: {@link FallbackVehicleSessionHandler}
* </li>
* <li>
* Then, all we do is listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
* Then, we listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
* </li>
*/
public final class FallbackVehicleSessionHandler extends FallbackSessionHandler {
public FallbackVehicleSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid,
final boolean forceCAPTCHA) {
super(user, username, uuid);
super(user, username);
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,
SPAWN_X_POSITION, DEFAULT_Y_COLLIDE_POSITION, SPAWN_Z_POSITION));
user.delayedWrite(spawnEntity);
user.delayedWrite(setPassengers);
user.getChannel().flush();
}
private final boolean forceCAPTCHA;
private boolean receivedPaddle, receivedInput;
private int paddlePackets, inputPackets, positionPackets;
private boolean expectMovement;
private static final int MINIMUM_REQUIRED_PACKETS = 2;
private void markSuccess() {
// Pass the player to the next best verification handler
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.
// Either send the player to the CAPTCHA or finish the verification.
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
// Send the player to the CAPTCHA handler
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
} else {
// The player has passed all checks
finishVerification();
}
}
// We can abuse the entity remove mechanic and check for position packets when the entity dies
private void move(double y) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
y -= 1.62f; // Account for 1.7 bounding box
}
// Check the Y position of the player
checkState(y <= IN_AIR_Y_POSITION, "invalid y position");
// Mark this check as successful if the player sent a few position packets
if (positionPackets++ > MINIMUM_REQUIRED_PACKETS) {
markSuccess();
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof PaddleBoatPacket) {
// Mark the PaddleBoat packet as received
receivedPaddle = true;
} else if (packet instanceof PlayerInputPacket) {
if (packet instanceof SetPlayerPositionRotationPacket) {
if (expectMovement) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
move(posRot.getY());
}
} else if (packet instanceof SetPlayerPositionPacket) {
if (expectMovement) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
move(position.getY());
}
} else if (packet instanceof PaddleBoatPacket) {
paddlePackets++;
} else if (packet instanceof PlayerInputPacket && !expectMovement) {
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
// Check if the player is sending invalid vehicle speed values
@@ -99,19 +123,23 @@ public final class FallbackVehicleSessionHandler extends FallbackSessionHandler
return;
}
// Mark this check as success if the player has sent
// two PlayerInput and a minimum of one PaddleBoat packets.
if (receivedInput && receivedPaddle) {
markSuccess();
// Once the player sent enough packets, go to the next stage
if (paddlePackets > MINIMUM_REQUIRED_PACKETS && inputPackets > MINIMUM_REQUIRED_PACKETS) {
// Remove the entity
// The next y coordinate the player will send is going
// to be the vehicle spawn position (y 64 in this case).
user.write(removeEntities);
// Listen for next movement packet(s)
expectMovement = true;
return;
}
// 1.8 and below do not have PaddleBoat packets,
// so we simply exempt them from the PaddleBoat check.
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
receivedPaddle = true;
paddlePackets++;
}
// Mark the PlayerInput packet as received
receivedInput = true;
inputPackets++;
}
}
}
@@ -63,7 +63,7 @@ public final class BlacklistCommand extends Subcommand {
}
// Display a warning if the IP is verified but being added to the blacklist
if (Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
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()),
@@ -79,16 +79,18 @@ public final class VerifiedCommand extends Subcommand {
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(rawAddress)) {
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(rawAddress);
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()),
@@ -105,6 +107,8 @@ public final class VerifiedCommand extends Subcommand {
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];
@@ -112,7 +116,7 @@ public final class VerifiedCommand extends Subcommand {
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
// Make sure the player is verified already
if (Sonar.get().getVerifiedPlayerController().has(rawAddress, uuid)) {
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())));
@@ -121,7 +125,7 @@ public final class VerifiedCommand extends Subcommand {
// Add verified player to the database
final long timestamp = System.currentTimeMillis();
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(rawAddress, uuid, timestamp));
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
@@ -28,7 +28,7 @@ import java.net.InetSocketAddress;
* Simple utility to determine if someone joins using GeyserMC
*/
@UtilityClass
public class GeyserDetection {
public class GeyserUtil {
// https://github.com/GeyserMC/Floodgate/blob/master/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java#L206
private final AttributeKey<Object> PLAYER_ATTRIBUTE = AttributeKey.valueOf("floodgate-player");
@@ -15,6 +15,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Copyright 2021 Andrew Steinborn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies
* or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package xyz.jonesdev.sonar.common.util;
import io.netty.buffer.ByteBuf;
@@ -22,6 +43,7 @@ import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.EncoderException;
import io.netty.util.Version;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.*;
import org.jetbrains.annotations.NotNull;
@@ -41,6 +63,25 @@ public class ProtocolUtil {
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
public static void checkNettyVersion() {
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
// We're pretty much only doing this to avoid incompatibilities on Bukkit,
// so we don't really care if the version couldn't be resolved.
if (version == null) {
return;
}
final String[] artifactVersion = version.artifactVersion().split("\\.");
final int major = Integer.parseInt(artifactVersion[0]);
final int minor = Integer.parseInt(artifactVersion[1]);
// Enforce Netty >4.1.x
if (major < 4 || (major == 4 && minor < 1)) {
throw new IllegalStateException("Your Netty version is too old to run Sonar! Please use Netty >4.1.x.");
}
}
public static int readVarInt(final ByteBuf byteBuf) {
int read = readVarIntSafely(byteBuf);
if (read == Integer.MIN_VALUE) {
@@ -97,6 +138,19 @@ public class ProtocolUtil {
}
}
private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
static {
for (int i = 0; i <= 32; ++i) {
VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
}
VARINT_EXACT_BYTE_LENGTHS[32] = 1;
}
public static int varIntBytes(final int value) {
return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
}
public static void writeVarLong(final ByteBuf byteBuf, final long value) {
// Peel the one and two byte count cases explicitly as they are the most common VarLong sizes
// that the proxy will write, to improve inlining.
@@ -160,9 +214,8 @@ public class ProtocolUtil {
}
}
public static void readUUID(final @NotNull ByteBuf byteBuf) {
byteBuf.readLong(); // least
byteBuf.readLong(); // most
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
return new UUID(byteBuf.readLong(), byteBuf.readLong());
}
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) {
@@ -43,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í
@@ -79,26 +81,24 @@ verification:
gravity:
# Měl by Sonar zkoušet pro gravitaci hráče (Doporučováno)
enabled: true
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
check-collisions: true
# Měl by Sonar přimět hráče, aby vyřešil CAPTCHA, pokud neuspějí při kontrole gravitace?
captcha-on-fail: false
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
max-movement-ticks: 8
# Gamemode hráče při verifikaci
# Typy: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
# - CREATIVE: životy a hunger neni zobrazen
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
gamemode: ADVENTURE
# Kontroluje, zda hráč naráží do bloků, které se vytvořily pod ním
collision:
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
enabled: true
# Jaký typ bloku by měl Sonar použít pro kolizní platformu?
# Typy: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Toto byste měli upravovat, pouze pokud víte, co děláte
collision-block-type: TRAPDOOR
# Kontroluje, zda klient odesílá správné pakety při montáži vozidla
vehicle:
# Typy: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Noví hráči budou vždy namontováni na vozidle (Doporučeno)
# - DURING_ATTACK: Noví hráči budou namontováni na vozidlo pouze během útoku
# - NEVER: Noví hráči nebudou nikdy namontováni na vozidlo
timing: ALWAYS
# Měl by sonar kontrolovat, zda hráč správně vstoupil na loď?
enabled: true
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
map-captcha:
@@ -173,24 +173,35 @@ 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
# Tutoriál: https://youtu.be/aSWrzQVjGnc?t=71
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
# Gamemode hráče při verifikaci
# Typy: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
# - CREATIVE: životy a hunger neni zobrazen
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
gamemode: ADVENTURE
# Sonar ukládá do mezipaměti většinu paketů, které odesílá klientovi, včetně paketu LoginSuccess.
# Které uživatelské jméno má Sonar použít pro paket LoginSuccess?
# Uživatelské jméno nebude pro hráče viditelné bez úpravy klienta
cached-username: "Sonar"
# Měl by Sonar posílat nové verifikace?
log-connections: true
# Měl by Sonar posílat nové verifikace při útoku?
@@ -220,10 +231,8 @@ verification:
# Kolikrát musí hráč selhat verifikaci aby byl přídán do černé listiny
# (Pokuď černou listinu nechcete dejte tuto hodnotu na 0)
blacklist-threshold: 2
# List ID protokolů (verzí), které nebudou kontrolovány Sonarem (bypass verifikace)
# List ID protokolů které se nebudou moct připojit vůbec
# Celý list ID protokolů můžete najít zde
# https://wiki.vg/Protocol_version_numbers
# Například, Minecraft 1.20 má ID 763.
whitelisted-protocols: []
# List ID protokolů které se nebudou moct připojit vůbec
blacklisted-protocols: []
@@ -40,8 +40,10 @@ attack-tracker:
# 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
# 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
@@ -76,26 +78,24 @@ verification:
gravity:
# Soll Sonar auf gültige Client-Gravitation prüfen? (Empfohlen)
enabled: true
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
check-collisions: true
# Soll Sonar den Spieler dazu bringen, ein CAPTCHA zu lösen, wenn er die Schwerkraftprüfung nicht besteht?
captcha-on-fail: false
# Maximale Anzahl an Ticks, die der Spieler fallen muss, um die Plattform erreichen zu dürfen
max-movement-ticks: 8
# Der Spielmodus des Spielers während der Überprüfung
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-Komponenten sind sichtbar
# - CREATIVE: Gesundheit und Hunger sind versteckt
# - ADVENTURE: alle UI-Komponenten sind sichtbar
gamemode: ADVENTURE
# Prüft, ob der Spieler mit Blöcken kollidiert, die unter ihm gespawnt sind
collision:
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
enabled: true
# Welche Art von Block sollte Sonar für die Kollisionsplattform verwenden?
# Mögliche Typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Sie sollten dies nur bearbeiten, wenn Sie wissen, was Sie tun
collision-block-type: TRAPDOOR
# Überprüft, ob der Spieler beim Einsteigen in ein Boot die richtigen Pakete sendet
vehicle:
# Mögliche Typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Neue Spieler werden immer auf einem Fahrzeug montiert (Empfohlen)
# - DURING_ATTACK: Neue Spieler werden nur während eines Angriffs auf ein Fahrzeug gesetzt
# - NEVER: Neue Spieler werden niemals in ein Fahrzeug eingebaut
timing: ALWAYS
# Sollte Sonar prüfen, ob der Spieler richtig in ein Fahrzeug einsteigt?
enabled: true
# Lässt den Spieler einen Code von einer virtuellen Karte im Chat eingeben
map-captcha:
@@ -170,12 +170,13 @@ verification:
# Maximale Anzahl an Login-Paketen, die der Spieler senden muss, um gekickt zu werden
max-login-packets: 256
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet,
# falls dieser die der Bot-Verifizierung erfolgreich besteht.
# Diese Funktion wurde von Mojang in Minecraft Version 1.20.5 eingeführt
# Anleitung: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sollte Sonar den Spieler auf den Ursprungsserver senden (anstatt ihn zu kicken)?
# Damit dies funktioniert, müssen Sie die Funktion in der Konfiguration Ihres Servers aktivieren
# Damit dies funktioniert, müssen Sie die Funktion in der Konfiguration Ihres Servers aktivieren.
# Darüber hinaus möchten Sie möglicherweise die Begrenzung der Anmelderate reduzieren
# wird von Velocity oder anderen Proxys/Plugins durchgeführt, da dies das verhindern könnte
# Spieler konnte nicht korrekt übertragen werden.
@@ -188,6 +189,16 @@ verification:
# Wenn Ihr Server keinen Port zum Herstellen einer Verbindung benötigt, können Sie diesen auf 25565 belassen.
destination-port: 25565
# Der Spielmodus des Spielers während der Überprüfung
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-Komponenten sind sichtbar
# - CREATIVE: Gesundheit und Hunger sind versteckt
# - ADVENTURE: alle UI-Komponenten sind sichtbar
gamemode: ADVENTURE
# Sonar speichert die meisten Pakete, die es an den Client sendet, einschließlich des LoginSuccess-Pakets.
# Welchen Benutzernamen soll Sonar für das LoginSuccess-Paket verwenden?
# Der Benutzername ist für den Spieler ohne Änderungen am Client nicht sichtbar
cached-username: "Sonar"
# Soll Sonar neue Überprüfungsversuche protokollieren?
log-connections: true
# Soll Sonar neue Überprüfungsversuche während Angriffen protokollieren?
@@ -217,10 +228,8 @@ verification:
# Wie oft muss ein Spieler die Überprüfung nicht bestehen, um auf die Schwarze Liste gesetzt zu werden?
# (Setzen Sie diesen Wert auf 0, um die Schwarze Liste vollständig zu deaktivieren)
blacklist-threshold: 2
# Liste von Protokoll-IDs, die von Sonar nicht überprüft werden (Überprüfungsumgehung)
# Liste von Protokoll-IDs, die den Server überhaupt nicht betreten dürfen
# Die vollständige Liste aller Protokoll-IDs finden Sie hier:
# https://wiki.vg/Protocol_version_numbers
# Zum Beispiel hat Minecraft 1.20 die ID 763.
whitelisted-protocols: []
# Liste von Protokoll-IDs, die den Server überhaupt nicht betreten dürfen
blacklisted-protocols: []
@@ -42,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
@@ -78,26 +80,24 @@ verification:
gravity:
# Should Sonar check for valid client gravity? (Recommended)
enabled: true
# Should Sonar check if the player collides with blocks? (Recommended)
check-collisions: true
# Should Sonar make the player solve a CAPTCHA if they fail the gravity check?
captcha-on-fail: false
# Maximum number of ticks the player has to fall to be allowed to hit the platform
max-movement-ticks: 8
# The gamemode of the player during verification
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: all UI components are visible
# - CREATIVE: health and hunger are hidden
# - ADVENTURE: all UI components are visible
gamemode: ADVENTURE
# Checks if the client sends proper packets when mounting a vehicle
# Checks if the player is colliding with blocks spawned below them
collision:
# Should Sonar check if the player collides with blocks? (Recommended)
enabled: true
# Which type of block should Sonar use for the collision platform?
# Possible types: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# You should only edit this if you know what you're doing
collision-block-type: TRAPDOOR
# Checks if the client sends proper packets when entering a boat
vehicle:
# Possible types: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: New players will always be mounted on a vehicle (Recommended)
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
# - NEVER: New players will never be mounted on a vehicle
timing: ALWAYS
# Should Sonar check if the player properly enters a boat?
enabled: true
# Make the player type a code from a virtual map in chat
map-captcha:
@@ -175,21 +175,32 @@ verification:
# If enabled, the player will be transferred back to the origin server
# after successfully passing the bot verification.
# This feature was introduced by Mojang in Minecraft version 1.20.5
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Should Sonar transfer the player to the origin server (instead of kicking them)?
# For this to work, you must enable the feature in your server's configuration
# For this to work, you must enable the feature in your server's configuration.
# Additionally, you might want to reduce the amount of login rate-limiting
# performed by Velocity or other proxies/plugins, as this might prevent the
# player from being transferred correctly.
enabled: false
# Which server should Sonar transfer the player to when the verification is passed?
# Please enter the server IP that is used by players to normally connect to your server.
# Please enter the server IP used by players to normally connect to your server.
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
destination-host: "play.my-server.com"
# Which port should Sonar use when transferring the player to the origin server?
# If your server does not need a port to connect to, you can leave this as 25565.
destination-port: 25565
# The gamemode of the player during verification
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: all UI components are visible
# - CREATIVE: health and hunger are hidden
# - ADVENTURE: all UI components are visible
gamemode: ADVENTURE
# Sonar caches most packets it sends to the client, including the LoginSuccess packet.
# Which username should Sonar use for the LoginSuccess packet?
# The username will not be visible to the player without client modifications
cached-username: "Sonar"
# Should Sonar log new verification attempts?
log-connections: true
# Should Sonar log new verification attempts during attacks?
@@ -219,10 +230,8 @@ verification:
# How often does a player have to fail the verification in order to be blacklisted?
# (Set this value to 0 to disable the blacklist entirely)
blacklist-threshold: 2
# List of protocol IDs which are not checked by Sonar (verification bypass)
# List of protocol IDs which are unable to join the server at all
# You can find the full list of all protocol IDs here:
# https://wiki.vg/Protocol_version_numbers
# For example, Minecraft 1.20 has the ID 763.
whitelisted-protocols: []
# List of protocol IDs which are unable to join the server at all
blacklisted-protocols: []
@@ -42,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
@@ -78,26 +80,24 @@ verification:
gravity:
# Sonar devrait-il vérifier la gravité du client ? (Recommandé)
enabled: true
# Sonar devrait-il vérifier si le joueur entre en collision avec des blocs ? (Recommandé)
check-collisions: true
# Sonar devrait-il faire résoudre un CAPTCHA au joueur s'il échoue à la vérification de la gravité ?
captcha-on-fail: false
# Nombre maximum de ticks que le joueur a pour tomber avant de pouvoir toucher la plateforme
max-movement-ticks: 8
# Le mode de jeu du joueur pendant la vérification
# Types possibles : SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL : tous les composants de l'interface utilisateur sont visibles
# - CREATIVE : la santé et la faim sont cachées
# - ADVENTURE : tous les composants de l'interface utilisateur sont visibles
gamemode: ADVENTURE
# Vérifie si le joueur entre en collision avec les blocs apparus en dessous de lui
collision:
# Sonar devrait-il vérifier si le joueur entre en collision avec des blocs ? (Recommandé)
enabled: true
# Quel type de bloc Sonar doit-il utiliser pour la plate-forme de collision ?
# Types possibles : STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Vous ne devriez modifier cela que si vous savez ce que vous faites
collision-block-type: TRAPDOOR
# Vérifie si le client envoie les paquets appropriés lorsqu'il monte dans un véhicule
vehicle:
# Types possibles : ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS : Les nouveaux joueurs seront toujours montés dans un véhicule (Recommandé)
# - DURING_ATTACK : Les nouveaux joueurs ne seront montés dans un véhicule que pendant une attaque
# - NEVER : Les nouveaux joueurs ne seront jamais montés dans un véhicule
timing: ALWAYS
# Sonar devrait-il vérifier si le joueur entre correctement dans un bateau ?
enabled: true
# Fait taper au joueur un code à partir d'une carte virtuelle dans le chat
map-captcha:
@@ -175,6 +175,7 @@ verification:
# S'il est activé, le joueur sera renvoyé au serveur d'origine
# après avoir passé avec succès la vérification du bot.
# Cette fonctionnalité a été introduite par Mojang dans la version Minecraft 1.20.5
# Tutoriel: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sonar devrait-il transférer le joueur au serveur d'origine (au lieu de l'expulser) ?
# Pour que cela fonctionne, vous devez activer la fonctionnalité dans la configuration de votre serveur
@@ -190,6 +191,16 @@ verification:
# Si votre serveur n'a pas besoin d'un port pour se connecter, vous pouvez laisser celui-ci comme 25565.
destination-port: 25565
# Le mode de jeu du joueur pendant la vérification
# Types possibles : SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL : tous les composants de l'interface utilisateur sont visibles
# - CREATIVE : la santé et la faim sont cachées
# - ADVENTURE : tous les composants de l'interface utilisateur sont visibles
gamemode: ADVENTURE
# Sonar met en cache la plupart des paquets qu'il envoie au client, y compris le paquet LoginSuccess.
# Quel nom d'utilisateur Sonar doit-il utiliser pour le paquet LoginSuccess ?
# Le nom d'utilisateur ne sera pas visible par le joueur sans modification du client
cached-username: "Sonar"
# Sonar devrait-il journaliser les nouvelles tentatives de vérification ?
log-connections: true
# Sonar devrait-il journaliser les nouvelles tentatives de vérification pendant les attaques ?
@@ -219,10 +230,8 @@ verification:
# Combien de fois un joueur doit-il échouer à la vérification pour être placé sur liste noire ?
# (Réglez cette valeur à 0 pour désactiver la liste noire entièrement)
blacklist-threshold: 2
# Liste des identifiants de protocole qui ne sont pas vérifiés par Sonar (bypass de vérification)
# Liste des identifiants de protocole qui ne peuvent pas du tout rejoindre le serveur
# Vous pouvez trouver la liste complète de tous les identifiants de protocole ici :
# https://wiki.vg/Protocol_version_numbers
# Par exemple, Minecraft 1.20 a l'ID 763.
whitelisted-protocols: []
# Liste des identifiants de protocole qui ne peuvent pas du tout rejoindre le serveur
blacklisted-protocols: []
@@ -15,12 +15,12 @@
# ზოგადი პარამეტრები Sonar-ისთვის
general:
# Should Sonar automatically check for new updates?
# Sonar uses the GitHub API to check if the current version is outdated
# შეამოწმოს თუ არა სონარმა ავტომატურად ახალი განახლებები?
# სონარი იყენებს GitHub API_ს რომ შეამოწმოს არის თუ არა ამჟამინდელი ვერსია ყველაზე ახალი.
check-for-updates: true
# უნდა ჩაწეროს თუ არა Sonar-მა მოთამაშეების IP მისამართები კონსოლში?
log-player-addresses: true
# მაქსიმალური ერთი და იგივე IP ზე ონლაინ მოთამაშეთა რაოდენობა
# ერთი და იგივე IP ზე ონლაინ მოთამაშეთა მაქსიმალური რაოდენობა
max-online-per-ip: 3
# მნიშვნელოვანი პარამეტრები თავდასხმის დაფიქსირებისთვის
@@ -31,7 +31,7 @@ attack-tracker:
# დრო, რის შემდეგაც შეტევა დასრულებული ჩაითვლება
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
min-attack-duration: 30000
# რამდენჯერ უნდა მოხდეს ზედა პარამეტრები, რათა ჩაითვალოს ეს თავდასხმად
# რამდენჯერ უნდა მოხდეს ინციდენტის დარეპორტება, რათა ჩაითვალოს ეს თავდასხმად
# ეს რიცხვი მოქმედებს ყალბი თავდასხმის შეტყობინებების გასაფილტრად
min-attack-threshold: 2
# დრო, რომელიც უნდა გავიდეს ახალი შეტევის დასადგენად
@@ -41,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 მონაცემთა ბაზის ავთენტიფიკაციისთვის
@@ -54,60 +56,58 @@ 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
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
gamemode: ADVENTURE
# Checks if the player is colliding with blocks spawned below them
collision:
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს? (რეკომენდებულია)
enabled: true
# Which type of block should Sonar use for the collision platform?
# შესაძლო ტიპები: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
collision-block-type: TRAPDOOR
# ამოწმებს, აგზავნის თუ არა კლიენტი შესაბამის პაკეტებს სატრანსპორტო საშუალებაზე დაჯდომის დროს
vehicle:
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: ახალი მოთამაშეები ყოველთვის დაჯდებიან სატრანსპორტო საშუალებაზე
# - DURING_ATTACK: ახალი მოთამაშეები მხოლოდ ბოტების შემოტევის დროს დაჯდებიან სატრანსპორტო საშუალებაზე
# - NEVER: ახალი მოთამაშეები არასოდეს დაჯდებიან სატრანსპორტო საშუალებაზე (რეკომენდებულია)
timing: ALWAYS
# Should Sonar check if the player properly enters a boat?
enabled: true
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებათ
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:
# მატებს შემთხვევითი ხაზების ეფექტს გამოსახულებაზე
@@ -127,9 +127,9 @@ verification:
# - 2: წრეები
# - 3: კვადრატები
shape: 2
# მანძილი დამახინჯებულ პიქსელებს/ობიექტებს შორის
# მანძილი არეულ პიქსელებს/ობიექტებს შორის
distance: 5
# დამახინჯებული პიქსელების/ობიექტების სიხშირე
# არეული პიქსელების/ობიექტების სიხშირე
density: 0.1
# შერევის ხარისხი ობიექტების ფერებს შორის
mix: 0.15
@@ -142,7 +142,7 @@ verification:
# თუ ეს გამორთულია, Sonar-ი გამოიყენებს შემთხვევით გრადიენტს ფერებისთვის
auto-color: false
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
# ეს ხდება ასინქრონულად უკანა ფონზე
# ეს ხდება ასინქრონულად (ანუ ამის გამოთვლას სერვერი არ დაელოდება) უკანა ფონზე
precompute: 500
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
@@ -153,32 +153,33 @@ 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 ვერსიაში
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაკიკვის ნაცვლად)?
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაგდების ნაცვლად)?
# ამისთვის, თქვენ უნდა ჩართოთ ეს ფუნქცია თქვენი სერვერის კონფიგურაციაში
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის rate-limiting ი
# რომელსაც ახორციელებს Velocity ან სხვა პროქსები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის სიხშირის ლიმიტი
# რომელსაც ახორციელებს Velocity ან სხვა პროქსიები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
# მოთამაშის სწორად გადაყვანას
enabled: false
# რომელ სერვერზე უნდა გადაიყვანოს Sonar-მა მოთამაშე ვერიფიკაციის გავლის შემდეგ?
@@ -189,22 +190,32 @@ verification:
# თუ თქვენს სერვერს არ სჭირდება პორტი შესასვლელად, შეგიძლიათ დატოვოთ ეს 25565-ზე
destination-port: 25565
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
gamemode: ADVENTURE
# Sonar caches most packets it sends to the client, including the LoginSuccess packet.
# Which username should Sonar use for the LoginSuccess packet?
# The username will not be visible to the player without client modifications
cached-username: "Sonar"
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
log-connections: true
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
log-during-attack: false
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
# ეს არ არის რეკომენდებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
debug-xyz-positions: false
# უნდა შეამოწმოს თუ არა Sonar-მა Geyser (Bedrock) მოთამაშეები?
# ეს ფუნქცია ექსპერიმენტულია და შეიძლება გამოიწვიოს პრობლემები
# თუ ეს გამორთულია, Bedrock მოთამაშეები გამოვლენილი იქნება
check-geyser-players: true
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
# დროის ოდენობა, რომელიც უნდა გავიდეს, სერვერთან კავშირის გარეშე, სანამ მოთამაშე დროებით გავარდება სერვერიდან.
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
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)
# დრო როდიდანაც სერვერმა შეწყვიტა პაკეტების გაგზავნა სანამ გააგდებს მოთამაშეს.
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
write-timeout: 10000
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
@@ -218,10 +229,8 @@ verification:
# რამდენად ხშირად უნდა გაიაროს მოთამაშემ ვერიფიკაცია წარუმატებლად, რომ ის მოხვდეს შავ სიაში?
# (დააყენეთ ეს 0 ზე, რომ მთლიანად გათიშოთ შავი სია)
blacklist-threshold: 2
# პროტოკოლის ID-ების სია, რომლებიც არ არის შემოწმებული Sonar-ის მიერ (დამოწმების შემოვლითი გზა)
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
# თქვენ შეგიძლიათ იპოვოთ ყველა პროტოკოლის ID-ების სრული სია აქ:
# https://wiki.vg/Protocol_version_numbers
# მაგალითად, Minecraft 1.20-ს აქვს ID 763.
whitelisted-protocols: []
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
blacklisted-protocols: []
@@ -42,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
@@ -78,26 +80,24 @@ verification:
gravity:
# Moet Sonar controleren op geldige zwaartekracht van de cliënt? (Aanbevolen)
enabled: true
# Moet Sonar controleren of de speler tegen blokken botst? (Aanbevolen)
check-collisions: true
# Moet Sonar de speler een CAPTCHA laten oplossen als hij niet slaagt voor de zwaartekracht controle?
captcha-on-fail: false
# Maximaal aantal ticks dat de speler moet laten vallen om het platform te mogen raken
max-movement-ticks: 8
# De spelmodus van de speler tijdens verificatie
# Mogelijke typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-componenten zijn zichtbaar
# - CREATIVE: gezondheid en honger zijn verborgen
# - ADVENTURE: alle UI-componenten zijn zichtbaar
gamemode: ADVENTURE
# Controleert of de speler botst met blokken die onder hem zijn gespawned
collision:
# Moet Sonar controleren of de speler tegen blokken botst? (Aanbevolen)
enabled: true
# Welk type blok moet Sonar gebruiken voor het platform?
# Mogelijke typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Je moet dit alleen bewerken als je weet wat je doet
collision-block-type: TRAPDOOR
# Controleert of de klant de juiste pakketten verzendt bij het monteren van een voertuig
vehicle:
# Mogelijke typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Nieuwe spelers worden altijd op een voertuig gemonteerd (Aanbevolen)
# - DURING_ATTACK: Nieuwe spelers worden alleen tijdens een aanval op een voertuig gemonteerd
# - NEVER: Nieuwe spelers zullen nooit op een voertuig worden gemonteerd
timing: ALWAYS
# Moet Sonar controleren of de speler goed in een boot stapt?
enabled: true
# Laat de speler een code van een virtuele kaart in de chat typen
map-captcha:
@@ -175,6 +175,7 @@ verification:
# Indien ingeschakeld, wordt de speler teruggestuurd naar de oorspronkelijke server
# nadat u de bot-verificatie met succes heeft doorstaan.
# Deze functie is geïntroduceerd door Mojang in Minecraft versie 1.20.5
# Handleiding: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Moet Sonar de speler overbrengen naar de oorspronkelijke server (in plaats van hem te schoppen)?
# Om dit te laten werken, moet u de functie inschakelen in de configuratie van uw server
@@ -190,6 +191,16 @@ verification:
# Als uw server geen poort nodig heeft om verbinding mee te maken, kunt u dit op 25565 laten staan.
destination-port: 25565
# De spelmodus van de speler tijdens verificatie
# Mogelijke typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-componenten zijn zichtbaar
# - CREATIVE: gezondheid en honger zijn verborgen
# - ADVENTURE: alle UI-componenten zijn zichtbaar
gamemode: ADVENTURE
# Sonar cached de meeste pakketten die het naar de client stuurt, inclusief het LoginSuccess pakket.
# Welke gebruikersnaam moet Sonar gebruiken voor het LoginSucces pakket?
# De gebruikersnaam zal niet zichtbaar zijn voor de speler zonder client aanpassingen
cached-username: "Sonar"
# Moet Sonar nieuwe verificatiepogingen registreren?
log-connections: true
# Moet Sonar nieuwe verificatiepogingen registreren tijdens aanvallen?
@@ -219,10 +230,8 @@ verification:
# Hoe vaak moet een speler de verificatie niet doorstaan om op de zwarte lijst te worden geplaatst?
# (Stel deze waarde in op 0 om de zwarte lijst volledig uit te schakelen)
blacklist-threshold: 2
# Lijst met protocol-ID's die niet door Sonar worden gecontroleerd (verificatie-bypass)
# Lijst met protocol-ID's die helemaal geen verbinding kunnen maken met de server
# De volledige lijst met alle protocol-ID's vindt u hier:
# https://wiki.vg/Protocol_version_numbers
# Minecraft 1.20 heeft bijvoorbeeld de ID 763.
whitelisted-protocols: []
# Lijst met protocol-ID's die helemaal geen verbinding kunnen maken met de server
blacklisted-protocols: []
@@ -0,0 +1,237 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# 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 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
# Sprawdza, czy gracz zderza się z blokami, które pojawiły się pod nim
collision:
# Czy Sonar powinien sprawdzać, czy gracz zderza się z blokami? (Zalecane)
enabled: true
# Jakiego typu bloku powinien użyć Sonar dla platformy kolizyjnej?
# Możliwe rodzaje: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Powinieneś to edytować tylko wtedy, gdy wiesz, co robisz
collision-block-type: TRAPDOOR
# Sprawdza, czy gracz wysyła odpowiednie pakiety podczas umieszczania na pojeździe.
vehicle:
# Czy Sonar powinien sprawdzać, czy gracz prawidłowo wsiada do łodzi?
enabled: true
# 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
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
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
# 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
# Sonar buforuje większość pakietów, które wysyła do klienta, w tym pakiet LoginSuccess.
# Jakiej nazwy użytkownika powinien używać Sonar dla pakietu LoginSuccess?
# Nazwa użytkownika nie będzie widoczna dla gracza bez modyfikacji klienta
cached-username: "Sonar"
# 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, które nie mogą w ogóle dołączyć do serwera
# 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.
blacklisted-protocols: []
@@ -42,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
@@ -78,26 +80,24 @@ verification:
gravity:
# Должен ли Sonar проверять наличие действительной гравитации клиента? (Рекомендуется)
enabled: true
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
check-collisions: true
# Должен ли Sonar заставлять игрока решать CAPTCHA, если он не проходит проверку на гравитацию?
captcha-on-fail: false
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
max-movement-ticks: 8
# Режим игрока во время проверки
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: все компоненты пользовательского интерфейса видны
# - CREATIVE: здоровье и голод скрыты
# - ADVENTURE: все компоненты пользовательского интерфейса видны
gamemode: ADVENTURE
# Проверяет, сталкивается ли игрок с блоками, порожденными под ним
collision:
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
enabled: true
# Какой тип блока следует использовать в Sonar для платформы столкновений?
# Возможные типы: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Рекомендуется редактировать только если вы знаете, что делаете
collision-block-type: TRAPDOOR
# Проверяет, отправляет ли клиент правильные пакеты при садке на транспортное средство
vehicle:
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Новые игроки всегда будут садиться на транспортное средство (Рекомендуется)
# - DURING_ATTACK: Новые игроки будут садиться на транспортное средство только во время атаки
# - NEVER: Новые игроки никогда не будут садиться на транспортное средство
timing: ALWAYS
# Должен ли сонар проверять, правильно ли игрок входит в лодку?
enabled: true
# Заставить игрока ввести код с картинки в чат
map-captcha:
@@ -174,6 +174,7 @@ verification:
# Если включено, игрок будет возвращен на исходный сервер
# после успешного прохождения проверки на бота.
# Эта функция была введена Mojang в версии Minecraft 1.20.5
# Учебник: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Должен ли Sonar переместить игрока на исходный сервер (вместо выкидывания его)?
# Для этого необходимо включить функцию в конфигурации вашего сервера
@@ -189,6 +190,16 @@ verification:
# Если ваш сервер не требует порта для подключения, вы можете оставить это как 25565.
destination-port: 25565
# Режим игрока во время проверки
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: все компоненты пользовательского интерфейса видны
# - CREATIVE: здоровье и голод скрыты
# - ADVENTURE: все компоненты пользовательского интерфейса видны
gamemode: ADVENTURE
# Sonar кэширует большинство пакетов, которые он отправляет клиенту, включая пакет LoginSuccess.
# Какое имя пользователя должен использовать Sonar для пакета LoginSuccess?
# Имя пользователя не будет видно игроку без модификации клиента
cached-username: "Sonar"
# Должен ли Sonar регистрировать новые попытки проверки?
log-connections: true
# Должен ли Sonar регистрировать новые попытки проверки во время атак?
@@ -218,10 +229,8 @@ verification:
# Как часто игрок должен проваливать проверку, чтобы попасть в черный список?
# (Установите это значение на 0, чтобы полностью отключить черный список)
blacklist-threshold: 2
# Список протоколов, которые не проверяются Sonar (обход проверки)
# Список протоколов, которые вообще не могут присоединиться к серверу
# Полный список всех идентификаторов протоколов можно найти здесь:
# https://wiki.vg/Protocol_version_numbers
# Например, Minecraft 1.20 имеет ID 763.
whitelisted-protocols: []
# Список протоколов, которые вообще не могут присоединиться к серверу
blacklisted-protocols: []
@@ -15,7 +15,7 @@
# 未归类的设置
general:
# Sonar 是否应该自动检查新的更新?
# Sonar 是否应该自动检查新的更新?
# Sonar 使用 GitHub API 检查当前版本是否过时
check-for-updates: true
# Sonar 应该在控制台中打印玩家的 IP 地址吗?
@@ -42,8 +42,10 @@ attack-tracker:
# 提示: IP 地址将以纯文本形式保存 请确保您的数据库安全!
database:
# 要连接到的数据库类型
# 可用类型: NONE (不使用数据库), MYSQL, MARIADB
# 可用类型: NONE (不使用数据库), MYSQL, MARIADB, H2
type: NONE
# Sonar 插件目录中用于 H2 数据库的文件
filename: verified_players
# 数据库的地址
host: localhost
# 数据库的端口
@@ -76,26 +78,24 @@ verification:
gravity:
# Sonar 是否应该检查玩家的下落移动? (推荐启用)
enabled: true
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
check-collisions: true
# 当玩家验证失败时 是否向他们发送验证码?
captcha-on-fail: false
# 玩家跌落在平台上多少tick后才能被算作站在平台上?
max-movement-ticks: 8
# 玩家在虚拟服务器中的游戏模式
# 可用类型: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: 生存模式, 所有UI可见
# - CREATIVE: 创造模式, 隐藏血量和饱食度
# - ADVENTURE: 冒险模式, 所有UI可见
gamemode: ADVENTURE
# 检查玩家是否与下方生成的方块发生碰撞
collision:
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
enabled: true
# Sonar 应该使用哪种类型的块作为碰撞平台?
# 可用类型: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# 只有当您知道您在做什么时, 才应该编辑它.
collision-block-type: TRAPDOOR
# 检查客户端坐在实体上时是否发送了正确的数据包
vehicle:
# 可用类型: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: 始终应用该检查
# - DURING_ATTACK: 仅在疑似遭受攻击时应用该检查
# - NEVER: 永远不应用该检查
timing: ALWAYS
# 声纳是否应该检查玩家是否正确进入船只?
enabled: true
# 地图验证码
map-captcha:
@@ -141,7 +141,8 @@ verification:
auto-color: false
# Sonar 应该一次准备多少个验证码?
precompute: 500
# Sonar 应该给玩家多少时间完成验证码? (该值为毫秒. 1秒等于1000毫秒.)
# Sonar 应该给玩家多少时间完成验证码?
# (该值为毫秒. 1秒等于1000毫秒.)
max-duration: 30000
# 玩家最多能输入多少次错误的验证码?
max-tries: 3
@@ -169,6 +170,7 @@ verification:
# 开启后 玩家将在验证成功后被重定向到指定的服务器
# 该功能由 Mojang 在 Minecraft 1.20.5 版本中引入
# 教程: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sonar 是否应该将玩家重定向到指定的服务器而不是踢出玩家?
# 为此您必须在服务器软件的配置文件中允许接受来自transfer的连接.
@@ -183,6 +185,16 @@ verification:
# 如果服务器地址不包含端口 请保留25565.
destination-port: 25565
# 玩家在虚拟服务器中的游戏模式
# 可用类型: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: 生存模式, 所有UI可见
# - CREATIVE: 创造模式, 隐藏血量和饱食度
# - ADVENTURE: 冒险模式, 所有UI可见
gamemode: ADVENTURE
# Sonar 缓存它发送到客户端的大部分数据包, 包括 LoginSuccess 数据包.
# Sonar 应该使用哪个用户名来接收 LoginSuccess 数据包?
# 如果不修改客户端, 用户名将不会对玩家可见
cached-username: "Sonar"
# 当 Sonar 尝试验证玩家时 是否在控制台中打印它?
log-connections: true
# Sonar 是否应该记录在攻击期间内的验证会话?
@@ -211,9 +223,7 @@ verification:
blacklist-time: 600000
# 玩家必须验证失败多少次才会被列入黑名单? (设置为0以完全禁用)
blacklist-threshold: 2
# Sonar 不应该检查哪些协议? (即 跳过检查)
# 黑名单中的协议将阻止所有使用该协议的客户端.
# 您可以在 https://wiki.vg/Protocol_version_numbers 找到所有协议号
# 例如 Minecraft 1.20 对应的协议号为763.
whitelisted-protocols: []
# 黑名单中的协议将阻止所有使用该协议的客户端.
blacklisted-protocols: []
@@ -0,0 +1,288 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licencováno pod GPL-3.0
#
# Zástupný text pro každý '<prefix>' v tomto konfiguračním souboru
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Zástupný text pro každou '<support-url>' v tomto konfiguračním souboru
support-url: https://jonesdev.xyz/discord/
# Zástupný text pro každý '<header>' v tomto konfiguračním souboru
header:
- <b><yellow>Sonar</b><reset>
- ''
# Zástupný text pro každý '<footer>' v tomto konfiguračním souboru
footer:
- <gray>Pokud se domníváte, že jde o chybu, kontaktujte administrátora.
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, ale je příliš mnoho online hráčů s jejich IP adresou
too-many-online-per-ip:
- '<header>'
- <red>Je příliš mnoho hráčů online s vaší IP adresou.
- '<footer>'
# Překlady pro příkazy
commands:
# Zpráva zobrazená, když se hráč pokusí spustit /sonar bez oprávnění
no-permission: '<prefix><red>Nemáte oprávnění k provedení tohoto příkazu.'
# Zpráva zobrazená, když někdo použije příkaz nesprávně
incorrect-usage: '<prefix><red>Použití: /sonar <subcommand-usage>'
# Zpráva zobrazená, když někdo zadá neplatnou IP adresu (Neplatný formát)
invalid-ip-address: '<prefix>IP adresa, kterou jste zadali, se zdá být neplatná.'
# Zpráva zobrazená, když konzole spustí příkaz, který je pouze pro hráče
player-only: '<prefix><red>Tento příkaz můžete provést pouze jako hráč.'
# Zpráva zobrazená, když hráč spustí příkaz, který je pouze pro konzoli
console-only: '<prefix><red>Z bezpečnostních důvodů můžete tento příkaz provést pouze přes konzoli.'
# Zpráva zobrazená, když hráč nemá oprávnění k provedení určitého podpříkazu
subcommand-no-permission: '<prefix><red>Nemáte oprávnění k provedení tohoto podpříkazu. <gray>(<permission>)'
# Překlady pro '/sonar'
main:
# Informační zpráva, která se zobrazí nad vším při spuštění hlavního příkazu
header:
- <yellow>Běží <b>Sonar</b> <version> na <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development a přispěvatelé Sonaru</click>
- ''
- <gray>Potřebujete pomoc nebo máte nějaké otázky?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Kliknutím otevřete Discord)'>Otevřete ticket na Discordu </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Kliknutím otevřete GitHub)'>nebo otevřete nový problém na GitHubu.
- ''
# Formátování seznamu podpříkazů zobrazených při spuštění hlavního příkazu
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Pouze hráči: </gray><only-players><br><gray>Vyžaduje konzoli: </gray><only-console><br><gray>Oprávnění: </gray><white><permission><br><gray>Aliasy: </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Formátování pro hodnoty ano (zaškrtnutí) a ne (křížek) v textu po najetí myší na podpříkazy
tick: '<green>✔</green>'
cross: '<red>✗</red>'
# Překlady pro '/sonar reload'
reload:
# Popis tohoto podpříkazu
description: "Znovu načíst všechny konfigurace"
# Zpráva zobrazená, když někdo začne znovu načítat Sonar
start: '<prefix>Znovu načítám Sonar...'
# Zpráva zobrazená, když Sonar dokončil znovu načítání
finish: '<prefix><green>Úspěšně znovu načteno <gray>(<time-taken>ms)'
# Překlady pro '/sonar verbose'
verbose:
# Popis tohoto podpříkazu
description: "Povolit nebo zakázat podrobný výpis Sonaru"
# Zpráva zobrazená, když se hráč přihlásí k odběru podrobného výpisu Sonaru
subscribe: '<prefix>Nyní zobrazujete podrobný výpis Sonaru.'
# Zpráva zobrazená, když se hráč odhlásí z odběru podrobného výpisu Sonaru
unsubscribe: '<prefix>Již nezobrazujete podrobný výpis Sonaru.'
# Překlady pro '/sonar notify'
notify:
# Popis tohoto podpříkazu
description: "Povolit nebo zakázat oznámení o útocích"
# Zpráva zobrazená, když se hráč přihlásí k odběru oznámení o útocích Sonaru
subscribe: '<prefix>Nyní zobrazujete oznámení o útocích Sonaru.'
# Zpráva zobrazená, když se hráč odhlásí z odběru oznámení o útocích Sonaru
unsubscribe: '<prefix>Již nezobrazujete oznámení o útocích Sonaru.'
# Překlady pro '/sonar blacklist'
blacklist:
# Popis tohoto podpříkazu
description: "Spravovat IP adresy na černé listině"
# Zpráva zobrazená, když se někdo pokusí vymazat černou listinu, ale je prázdná
empty: '<prefix>Černá listina je momentálně prázdná. Proto nebyly z černé listiny odstraněny žádné IP adresy.'
# Zpráva zobrazená, když někdo vymaže černou listinu
clear: '<prefix>Úspěšně jste odstranili celkem <amount> IP adres(y) z černé listiny.'
# Zpráva zobrazená, když někdo zkontroluje velikost černé listiny
size: '<prefix>Černá listina momentálně obsahuje <amount> IP adres(y).'
# Zpráva zobrazená, když někdo odstraní IP adresu z černé listiny
remove: '<prefix>Úspěšně odstraněno <ip> z černé listiny.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu
add: '<prefix>Úspěšně přidáno <ip> na černou listinu.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, která je ověřená
add-warning: '<prefix><red>Varování: <white><ip> je momentálně na bílé listině. Zvažte odstranění IP adresy ze seznamu ověřených hráčů, abyste se vyhnuli potenciálním problémům.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, ale již je na černé listině
ip-duplicate: '<prefix>IP adresa, kterou jste zadali, je již na černé listině.'
# Zpráva zobrazená, když někdo odstraní IP adresu z černé listiny, ale není na černé listině
ip-not-found: '<prefix>IP adresa, kterou jste zadali, není na černé listině.'
# Překlady pro '/sonar verified'
verified:
# Popis tohoto podpříkazu
description: "Spravovat ověřené IP adresy"
# Zpráva zobrazená, když se někdo pokusí vymazat seznam ověřených hráčů, ale je prázdný
empty: '<prefix>Seznam ověřených hráčů je momentálně prázdný. Proto nebyli žádní hráči odebráni z ověření.'
# Zpráva zobrazená, když někdo vymaže seznam ověřených hráčů
clear: '<prefix>Úspěšně jste odebrali ověření celkem <amount> unikátním hráčům.'
# Zpráva zobrazená, když někdo zkontroluje velikost seznamu ověřených hráčů
size: '<prefix>Momentálně je ověřeno <amount> unikátních hráčů.'
# Zpráva zobrazená, když někdo odebere ověření IP adrese
remove: '<prefix>Úspěšně odebráno ověření pro <ip>.'
# Zpráva zobrazená, když někdo ověří IP adresu
add: '<prefix>Úspěšně ověřeno <ip>.'
# Zpráva zobrazená, když se někdo pokusí ověřit IP adresu/UUID, které jsou již ověřeny
already: '<prefix>IP adresa a UUID, které jste zadali, jsou již ověřeny.'
# Zpráva zobrazená, když někdo odebere ověření IP adrese, ale není ověřena
ip-not-found: '<prefix>IP adresa, kterou jste zadali, není ověřena.'
# Zpráva zobrazená, když někdo vyhledá historii ověření IP adresy
history: '<prefix><yellow>Předchozí UUID pro <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Překlady pro '/sonar statistics'
statistics:
# Popis tohoto podpříkazu
description: "Zobrazit statistiky relace tohoto serveru"
# Informační zpráva, která se zobrazí nad vším při zobrazení statistik
header: '<prefix>Zobrazuji <statistics-type> statistiky pro tuto relaci:'
# Zpráva zobrazená, když se hráč pokusí zobrazit neznámou statistiku
unknown-type: '<prefix><red>Neznámý typ statistiky! Dostupné statistiky: <gray><statistics>'
# Formát zprávy obecných statistik
general:
- ' <dark_aqua>▪ <gray>Ověřené IP adresy: <white><verified>'
- ' <dark_aqua>▪ <gray>Ověřující se IP adresy: <white><verifying>'
- ' <dark_aqua>▪ <gray>IP adresy na černé listině: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Aktuálně ve frontě přihlášení: <white><queued>'
- ' <dark_aqua>▪ <gray>Celkový počet ne-unikátních připojení: <white><total-joins>'
- ' <dark_aqua>▪ <gray>Celkový počet pokusů o ověření: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Celkový počet neúspěšných ověření: <white><total-failed>'
- ' <dark_aqua>▪ <gray>Doba provozu serveru: <white><server-uptime>'
# Formát zprávy statistik CPU
cpu:
- ' <dark_aqua>▪ <gray>Využití CPU procesem právě teď: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>Využití CPU systémem právě teď: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Využití CPU procesem na jádro: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Využití CPU systémem na jádro: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Celkový počet virtuálních CPU: <white><virtual-core-count>'
# Formát zprávy statistik paměti
memory:
- ' <dark_aqua>▪ <gray>Celková volná paměť: <white><free-memory>'
- ' <dark_aqua>▪ <gray>Celková použitá paměť: <white><used-memory>'
- ' <dark_aqua>▪ <gray>Celková maximální paměť: <white><max-memory>'
- ' <dark_aqua>▪ <gray>Celková alokovaná paměť: <white><total-memory>'
# Formát zprávy statistik sítě
network:
- ' <dark_aqua>▪ <gray>Aktuální příchozí využitá šířka pásma: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Aktuální odchozí využitá šířka pásma: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Celková příchozí využitá šířka pásma: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Celková odchozí využitá šířka pásma: <white><incoming-traffic-ttl>'
# Překlady pro '/sonar dump'
dump:
# Popis tohoto podpříkazu
description: "Vypsat informace pro vývojáře"
# Formát informací odeslaných do konzole
log: "Vygenerovaný výpis: <dumped-json-data>"
# Překlady pro všechny zprávy během procesu ověření
verification:
# Překlady pro všechny ladící zprávy během ověření
logs:
# Zpráva zaznamenána do konzole, kdykoli se nový hráč připojí k serveru
connection: '<username><ip> (<protocol>) se připojil.'
# Zpráva zaznamenána do konzole, kdykoli hráč selže při ověření
failed: '<ip> (<protocol>) neprošel kontrolou bota z důvodu: <reason>'
# Zpráva zaznamenána do konzole, kdykoli je hráč zařazen na černou listinu
blacklisted: '<ip> (<protocol>) byl zařazen na černou listinu kvůli příliš mnoha neúspěšným pokusům'
# Zpráva zaznamenána do konzole, kdykoli je hráč ověřen
successful: '<username> byl úspěšně ověřen (<time-taken>s!).'
# Zprávy, které se zobrazují hráči, když má zadat CAPTCHA
captcha:
# Zpráva zobrazená hráči, když má zadat odpověď na captchu
enter-code: '<prefix><green>Prosím, zadejte do chatu kód, který je zobrazen na mapě.'
# Zpráva zobrazená hráči, když zadá špatnou odpověď do chatu
incorrect: '<prefix><red>Zadali jste špatný kód. Zkuste to prosím znovu.'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, zatímco captcha ještě není připravena
currently-preparing:
- '<header>'
- <yellow>Vaše anti-bot data ještě nejsou připravena.
- <gray>Počkejte prosím několik sekund, než se pokusíte znovu ověřit.
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo znovu připojí příliš rychle během ověření
too-fast-reconnect:
- '<header>'
- <gold>Připojili jste se příliš rychle, zkuste to později.
- <gray>Počkejte prosím několik sekund, než se pokusíte znovu ověřit.
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, ale již probíhá jeho ověření
already-verifying:
- '<header>'
- <red>Vaše IP adresa je právě ověřována.
- <red>Počkejte prosím několik sekund, než se pokusíte znovu ověřit.
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, ale je již ve frontě na ověření
already-queued:
- '<header>'
- <red>Vaše IP adresa je momentálně ve frontě na ověření.
- <red>Počkejte prosím několik minut, než se pokusíte znovu ověřit.
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí, ale je dočasně na černé listině
blacklisted:
- '<header>'
- <red>Momentálně vám je odepřen vstup na server.
- <red>Počkejte prosím několik minut, abyste se mohli znovu připojit k serveru.
- <gold>Falešně pozitivní? <gray><support-url>
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí s neplatným uživatelským jménem
invalid-username:
- '<header>'
- <red>Vaše uživatelské jméno obsahuje neplatné znaky.
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když se někdo připojí s verzí na černé listině
blacklisted-protocol:
- '<header>'
- <red>Používáte verzi, která není na našem serveru povolena.
- <gold>Potřebujete pomoc s přihlášením? <gray><support-url>
- '<footer>'
# Zpráva o odpojení, která se zobrazí, když někdo úspěšně projde ověřením
success:
- '<header>'
- <green>Úspěšně jste prošli ověřením.
- <white>Nyní se můžete připojit k serveru po opětovném připojení.
# Zpráva o odpojení, která se zobrazí, když někdo selže při ověření
failed:
- '<header>'
- <red>Neprošli jste ověřením bota.
- <gray>Počkejte prosím několik sekund, než se pokusíte znovu ověřit.
- <gold>Potřebujete pomoc s přihlášením? <gray><support-url>
- '<footer>'
# Překlady pro podrobné zprávy Sonaru v panelu akcí
verbose:
layout:
# Rozložení pro podrobné zprávy Sonaru v panelu akcí
normal: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Přihlášení/s <white><logins-per-second> <dark_aqua>╺ <gray>Ověřování <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Rozložení pro podrobné zprávy Sonaru v panelu akcí během útoku
attack: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Přihlášení/s <white><logins-per-second> <dark_aqua>╺ <gray>Fronta <white><queued> <dark_aqua>╺ <gray>Ověřování <white><verifying> <dark_aqua>╺ <gray>Trvání <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animace pro panel akcí
# Alternativy:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Překlady pro oznámení o útoku Sonaru
notifications:
# Rozložení pro titulek oznámení o útoku Sonaru
title: <yellow><b>Sonar<reset>
# Rozložení pro podtitulek oznámení o útoku Sonaru
subtitle: <gray>Byl detekován útok botů
# Rozložení pro oznámení o útoku Sonaru v chatu
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Byl detekován útok botů.
- ' <dark_aqua>▪ <gray>Přihlášení za sekundu: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Ověřující se hráči: <white><verifying>'
- ' <dark_aqua>▪ <gray>Hráči ve frontě: <white><queued>'
- ''
- <gray>Zobrazit více informací pomocí <white><click:run_command:'/sonar verbose'><hover:show_text:'(Klikněte pro spuštění)'>/sonar verbose</hover></click><gray> nebo <white><click:run_command:'/sonar stats'><hover:show_text:'(Klikněte pro spuštění)'>/sonar stats</hover></click><gray>.
- ''
@@ -0,0 +1,288 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Butuh bantuan atau memiliki pertanyaan? https://jonesdev.xyz/discord
# Kode sumber: https://github.com/jonesdevelopment/sonar
# Hak cipta Jones Development dan Kontributor Sonar, dilisensikan di bawah GPL-3.0
#
# Placeholder untuk setiap '<prefix>' dalam file konfigurasi ini
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Placeholder untuk setiap '<support-url>' dalam file konfigurasi ini
support-url: https://jonesdev.xyz/discord/
# Placeholder untuk setiap '<header>' dalam file konfigurasi ini
header:
- <b><yellow>Sonar</b><reset>
- ''
# Placeholder untuk setiap '<footer>' dalam file konfigurasi ini
footer:
- <gray>Jika Anda yakin ini adalah kesalahan, hubungi administrator.
# Pesan pemutusan yang ditampilkan saat seseorang bergabung, tetapi ada terlalu banyak pemain online dengan alamat IP mereka
too-many-online-per-ip:
- '<header>'
- <red>Ada terlalu banyak pemain online dengan alamat IP Anda.
- '<footer>'
# Terjemahan untuk perintah
commands:
# Pesan yang ditampilkan saat pemain mencoba menjalankan /sonar tanpa izin
no-permission: '<prefix><red>Anda tidak memiliki izin untuk menjalankan perintah ini.'
# Pesan yang ditampilkan saat seseorang menggunakan perintah dengan salah
incorrect-usage: '<prefix><red>Penggunaan: /sonar <subcommand-usage>'
# Pesan yang ditampilkan saat seseorang memberikan alamat IP yang tidak valid (Format tidak valid)
invalid-ip-address: '<prefix>Alamat IP yang Anda berikan tampaknya tidak valid.'
# Pesan yang ditampilkan saat konsol menjalankan perintah yang hanya untuk pemain
player-only: '<prefix><red>Anda hanya dapat menjalankan perintah ini sebagai pemain.'
# Pesan yang ditampilkan saat pemain menjalankan perintah yang hanya untuk konsol
console-only: '<prefix><red>Demi alasan keamanan, Anda hanya dapat menjalankan perintah ini melalui konsol.'
# Pesan yang ditampilkan saat pemain tidak memiliki izin untuk menjalankan sub-perintah tertentu
subcommand-no-permission: '<prefix><red>Anda tidak memiliki izin untuk menjalankan sub-perintah ini. <gray>(<permission>)'
# Terjemahan untuk '/sonar'
main:
# Pesan informasi yang ditampilkan di atas segalanya saat menjalankan perintah utama
header:
- <yellow>Menjalankan <b>Sonar</b> <version> di <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development dan Kontributor Sonar</click>
- ''
- <gray>Butuh bantuan atau memiliki pertanyaan?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Klik untuk membuka Discord)'>Buka tiket di Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Klik untuk membuka GitHub)'>atau buka masalah baru di GitHub.
- ''
# Format daftar sub-perintah yang ditampilkan saat menjalankan perintah utama
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Hanya pemain: </gray><only-players><br><gray>Membutuhkan konsol: </gray><only-console><br><gray>Izin: </gray><white><permission><br><gray>Alias: </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Format untuk nilai ya (centang) dan tidak (silang) dalam teks hover sub-perintah
tick: '<green>✔</green>'
cross: '<red>✗</red>'
# Terjemahan untuk '/sonar reload'
reload:
# Deskripsi untuk sub-perintah ini
description: "Muat ulang semua konfigurasi"
# Pesan yang ditampilkan saat seseorang mulai memuat ulang Sonar
start: '<prefix>Memuat ulang Sonar...'
# Pesan yang ditampilkan saat Sonar selesai memuat ulang
finish: '<prefix><green>Berhasil memuat ulang <gray>(<time-taken>ms)'
# Terjemahan untuk '/sonar verbose'
verbose:
# Deskripsi untuk sub-perintah ini
description: "Aktifkan atau nonaktifkan verbose Sonar"
# Pesan yang ditampilkan saat pemain berlangganan verbose Sonar
subscribe: '<prefix>Anda sekarang melihat verbose Sonar.'
# Pesan yang ditampilkan saat pemain berhenti berlangganan verbose Sonar
unsubscribe: '<prefix>Anda tidak lagi melihat verbose Sonar.'
# Terjemahan untuk '/sonar notify'
notify:
# Deskripsi untuk sub-perintah ini
description: "Aktifkan atau nonaktifkan notifikasi serangan"
# Pesan yang ditampilkan saat pemain berlangganan notifikasi serangan Sonar
subscribe: '<prefix>Anda sekarang melihat notifikasi serangan Sonar.'
# Pesan yang ditampilkan saat pemain berhenti berlangganan notifikasi serangan Sonar
unsubscribe: '<prefix>Anda tidak lagi melihat notifikasi serangan Sonar.'
# Terjemahan untuk '/sonar blacklist'
blacklist:
# Deskripsi untuk sub-perintah ini
description: "Kelola alamat IP yang masuk daftar hitam"
# Pesan yang ditampilkan saat seseorang mencoba mengosongkan daftar hitam tetapi kosong
empty: '<prefix>Daftar hitam saat ini kosong. Oleh karena itu, tidak ada alamat IP yang dihapus dari daftar hitam.'
# Pesan yang ditampilkan saat seseorang mengosongkan daftar hitam
clear: '<prefix>Anda berhasil menghapus total <amount> alamat IP dari daftar hitam.'
# Pesan yang ditampilkan saat seseorang memeriksa ukuran daftar hitam
size: '<prefix>Daftar hitam saat ini berisi <amount> alamat IP.'
# Pesan yang ditampilkan saat seseorang menghapus alamat IP dari daftar hitam
remove: '<prefix>Berhasil menghapus <ip> dari daftar hitam.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam
add: '<prefix>Berhasil menambahkan <ip> ke daftar hitam.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam yang diverifikasi
add-warning: '<prefix><red>Peringatan: <white><ip> saat ini di daftar putih. Pertimbangkan untuk menghapus alamat IP dari daftar pemain yang diverifikasi untuk menghindari masalah potensial.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam, tetapi sudah masuk daftar hitam
ip-duplicate: '<prefix>Alamat IP yang Anda berikan sudah masuk daftar hitam.'
# Pesan yang ditampilkan saat seseorang menghapus alamat IP dari daftar hitam, tetapi tidak masuk daftar hitam
ip-not-found: '<prefix>Alamat IP yang Anda berikan tidak masuk daftar hitam.'
# Translations for '/sonar verified'
verified:
# Deskripsi untuk sub-perintah ini
description: "Kelola alamat IP yang diverifikasi"
# Pesan yang ditampilkan saat seseorang mencoba mengosongkan daftar pemain yang diverifikasi tetapi kosong
empty: '<prefix>Daftar pemain yang diverifikasi saat ini kosong. Oleh karena itu, tidak ada pemain yang dibatalkan verifikasinya.'
# Pesan yang ditampilkan saat seseorang mengosongkan daftar pemain yang diverifikasi
clear: '<prefix>Anda berhasil membatalkan verifikasi total <amount> pemain unik.'
# Pesan yang ditampilkan saat seseorang memeriksa ukuran daftar pemain yang diverifikasi
size: '<prefix>Saat ini ada <amount> pemain unik yang diverifikasi.'
# Pesan yang ditampilkan saat seseorang membatalkan verifikasi alamat IP
remove: '<prefix>Berhasil membatalkan verifikasi <ip>.'
# Pesan yang ditampilkan saat seseorang memverifikasi alamat IP
add: '<prefix>Berhasil memverifikasi <ip>.'
# Pesan yang ditampilkan saat seseorang mencoba memverifikasi alamat IP/UUID yang sudah diverifikasi
already: '<prefix>Alamat IP dan UUID yang Anda berikan sudah diverifikasi.'
# Pesan yang ditampilkan saat seseorang membatalkan verifikasi alamat IP, tetapi tidak diverifikasi
ip-not-found: '<prefix>Alamat IP yang Anda berikan tidak diverifikasi.'
# Pesan yang ditampilkan saat seseorang melihat riwayat verifikasi alamat IP
history: '<prefix><yellow>UUID sebelumnya untuk <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Terjemahan untuk '/sonar statistics'
statistics:
# Deskripsi untuk sub-perintah ini
description: "Tampilkan statistik sesi server ini"
# Pesan informasi yang ditampilkan di atas segalanya saat melihat statistik
header: '<prefix>Menampilkan statistik <statistics-type> untuk sesi ini:'
# Pesan yang ditampilkan saat pemain mencoba melihat statistik yang tidak dikenal
unknown-type: '<prefix><red>Tipe statistik tidak dikenal! Statistik yang tersedia: <gray><statistics>'
# Format pesan statistik umum
general:
- ' <dark_aqua>▪ <gray>Alamat IP yang diverifikasi: <white><verified>'
- ' <dark_aqua>▪ <gray>Memverifikasi alamat IP: <white><verifying>'
- ' <dark_aqua>▪ <gray>Alamat IP yang masuk daftar hitam: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Antrian login saat ini: <white><queued>'
- ' <dark_aqua>▪ <gray>Total join non-unik: <white><total-joins>'
- ' <dark_aqua>▪ <gray>Total percobaan verifikasi: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Total verifikasi yang gagal: <white><total-failed>'
- ' <dark_aqua>▪ <gray>Waktu aktif server: <white><server-uptime>'
# Format pesan statistik CPU
cpu:
- ' <dark_aqua>▪ <gray>Penggunaan CPU proses saat ini: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>Penggunaan CPU sistem saat ini: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Penggunaan CPU proses per-core: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Penggunaan CPU sistem per-core: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Total jumlah vCPU: <white><virtual-core-count>'
# Format pesan statistik memori
memory:
- ' <dark_aqua>▪ <gray>Total memori bebas: <white><free-memory>'
- ' <dark_aqua>▪ <gray>Total memori terpakai: <white><used-memory>'
- ' <dark_aqua>▪ <gray>Total memori maksimum: <white><max-memory>'
- ' <dark_aqua>▪ <gray>Total memori dialokasikan: <white><total-memory>'
# Format pesan statistik jaringan
network:
- ' <dark_aqua>▪ <gray>Bandwidth masuk saat ini: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Bandwidth keluar saat ini: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Total bandwidth masuk: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Total bandwidth keluar: <white><outgoing-traffic-ttl>'
# Terjemahan untuk '/sonar dump'
dump:
# Deskripsi untuk sub-perintah ini
description: "Cetak informasi pengembang"
# Format informasi yang dikirim ke konsol
log: "Dump yang dihasilkan: <dumped-json-data>"
# Terjemahan untuk semua pesan selama proses verifikasi
verification:
# Terjemahan untuk semua pesan debug selama proses verifikasi
logs:
# Pesan yang dicatat ke konsol setiap kali pemain baru bergabung ke server
connection: '<username><ip> (<protocol>) telah terhubung.'
# Pesan yang dicatat ke konsol setiap kali pemain gagal verifikasi
failed: '<ip> (<protocol>) telah gagal dalam pemeriksaan bot untuk: <reason>'
# Pesan yang dicatat ke konsol setiap kali pemain masuk daftar hitam
blacklisted: '<ip> (<protocol>) masuk daftar hitam karena terlalu banyak percobaan yang gagal'
# Pesan yang dicatat ke konsol setiap kali pemain berhasil diverifikasi
successful: '<username> berhasil diverifikasi dalam waktu (<time-taken>s!).'
# Pesan yang ditampilkan kepada pemain saat mereka seharusnya memasukkan CAPTCHA
captcha:
# Pesan yang ditampilkan kepada pemain saat mereka harus memasukkan jawaban untuk captcha
enter-code: '<prefix><green>Silakan masukkan kode di chat yang ditampilkan di peta.'
# Pesan yang ditampilkan kepada pemain saat mereka memasukkan jawaban yang salah di chat
incorrect: '<prefix><red>Anda memasukkan kode yang salah. Silakan coba lagi.'
# Pesan putus yang ditampilkan saat seseorang bergabung sementara captcha belum disiapkan
currently-preparing:
- '<header>'
- <yellow>Data anti-bot Anda belum disiapkan.
- <gray>Silakan tunggu beberapa detik sebelum mencoba verifikasi lagi.
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung kembali terlalu cepat selama verifikasi
too-fast-reconnect:
- '<header>'
- <gold>Anda terhubung kembali terlalu cepat, coba lagi nanti.
- <gray>Silakan tunggu beberapa detik sebelum mencoba verifikasi lagi.
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung tetapi sudah dalam proses verifikasi
already-verifying:
- '<header>'
- <red>Alamat IP Anda saat ini sedang diverifikasi.
- <red>Silakan tunggu beberapa detik sebelum mencoba verifikasi lagi.
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung tetapi sudah antri untuk verifikasi
already-queued:
- '<header>'
- <red>Alamat IP Anda saat ini sedang antri untuk verifikasi.
- <red>Silakan tunggu beberapa menit sebelum mencoba verifikasi lagi.
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung tetapi sedang masuk daftar hitam sementara
blacklisted:
- '<header>'
- <red>Anda saat ini ditolak untuk masuk ke server.
- <red>Silakan tunggu beberapa menit agar bisa bergabung ke server lagi.
- <gold>Positif palsu? <gray><support-url>
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung dengan username yang tidak valid
invalid-username:
- '<header>'
- <red>Username Anda mengandung karakter yang tidak valid.
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang bergabung dengan versi yang masuk daftar hitam
blacklisted-protocol:
- '<header>'
- <red>Anda menggunakan versi yang tidak diizinkan di server kami.
- <gold>Butuh bantuan masuk? <gray><support-url>
- '<footer>'
# Pesan putus yang ditampilkan saat seseorang berhasil verifikasi
success:
- '<header>'
- <green>Anda telah berhasil melewati verifikasi.
- <white>Anda sekarang dapat bermain di server saat Anda terhubung kembali.
# Pesan putus yang ditampilkan saat seseorang gagal verifikasi
failed:
- '<header>'
- <red>Anda telah gagal dalam verifikasi bot.
- <gray>Silakan tunggu beberapa detik sebelum mencoba verifikasi lagi.
- <gold>Butuh bantuan masuk? <gray><support-url>
- '<footer>'
# Terjemahan untuk pesan verbose actionbar Sonar
verbose:
layout:
# Tata letak untuk actionbar verbose Sonar
normal: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Verifikasi <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Tata letak untuk actionbar verbose Sonar selama serangan
attack: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Antrian <white><queued> <dark_aqua>╺ <gray>Verifikasi <white><verifying> <dark_aqua>╺ <gray>Durasi <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animasi untuk action bar
# Alternatif:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Terjemahan untuk pesan notifikasi serangan Sonar
notifications:
# Tata letak untuk judul notifikasi serangan Sonar
title: <yellow><b>Sonar<reset>
# Tata letak untuk subjudul notifikasi serangan Sonar
subtitle: <gray>Serangan bot terdeteksi
# Tata letak untuk notifikasi serangan Sonar di chat
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Serangan bot terdeteksi.
- ' <dark_aqua>▪ <gray>Login per detik: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Pemain yang diverifikasi: <white><verifying>'
- ' <dark_aqua>▪ <gray>Pemain yang antri: <white><queued>'
- ''
- <gray>Lihat lebih banyak informasi menggunakan <white><click:run_command:'/sonar verbose'><hover:show_text:'(Klik untuk menjalankan)'>/sonar verbose</hover></click><gray> atau <white><click:run_command:'/sonar stats'><hover:show_text:'(Klik untuk menjalankan)'>/sonar stats</hover></click><gray>.
- ''
@@ -0,0 +1,289 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# გჭირდება დახმარება ან გაქვს კითხვები? დაგვიკავშირდი https://jonesdev.xyz/discord
# კოდის წყარო: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# ცვლადი ყველა '<prefix>' სთვის ამ ფაილში.
prefix: <b><yellow>Sonar</b> <gray>» <white>
# ცვლადი ყველა '<support-url>' სთვის ამ ფაილში.
support-url: https://jonesdev.xyz/discord/
# ცვლადი ყველა '<header>' სთვის ამ ფაილში.
header:
- <b><yellow>Sonar</b><reset>
- ''
# ცვლადი ყველა '<footer>' სთვის ამ ფაილში.
footer:
- <gray>თუ გჯერათ რომ შეცდომა მოხდა, დაუკავშირდით ადმინისტრატორს.
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როდესაც ვინმე შემოვა, მაგრამ სერვერზე ბევრი მოთამაშეა მისი IP მისამართით
too-many-online-per-ip:
- '<header>'
- <red>ძალიან ბევრი ადამიანია თქვენი IP მისამართით შემოსული.
- '<footer>'
# თარგმანი ბრძანებებისთვის
commands:
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც მოთამაშე ცდის /sonar ს ამის უფლების გარეშე
no-permission: '<prefix><red>თქვენ არ გაქვთ უფლება ამ ბრძანების გასაცემად.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც ბრძანება არასწორად იქნება გაცემული.
incorrect-გამოყენება: '<prefix><red>გამოყენება: /sonar <subcommand-გამოყენება>'
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც ვინმე გვაწვდის არასწორ IP მისამართს (არასწორი ფორმატით)
invalid-ip-address: '<prefix>თქვენი IP მისამართი, როგორც ჩანს არასწორია.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც კონსოლი ცდის იმ ბრძანების გაცემას რომლის გაცემაც მხოლოდ მოთამაშეს შეუძლია.
player-only: '<prefix><red>თქვენ შეგიძლიათ ამ ბრძანების გაცემა მხოლოდ, როგორც მოთამაშეს.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც მოთამაშე გასცემს ბრძანებას რომლის გაცემაც მხოლოდ კონსოლს შეუძლია.
console-only: '<prefix><red>უსაფრთხოების მიზეზების გამო, ამ ბრძანების შესრულება შეგიძლიათ მხოლოდ კონსოლიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როდესაც მოთამაშე ცდის იმ ქვე-ბრძანების გაცემას რომლის უფლებაც არ აქვს.
subcommand-no-permission: '<prefix><red>თქვენ არ გაქვთ უფლება რომ გასცეთ ეს ქვე-ბრძანება. <gray>(<permission>)'
# თარგმანი ბრძანება '/sonar' ისთვის
main:
# ინფორმაციული მესიჯი რომელიც იქნება ნაჩვენები ყველაფრის ზემოთ როდესაც ბრძანება გაიცემა.
header:
- <yellow>გამოიყენება <b>Sonar</b> <version> <platform>_ზე.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development and Sonar Contributors</click>
- ''
- <gray>გჭირდებათ დახმარება ან გაქვთ რამე კითხვა?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(დააჭირეთ რომ გახსნათ Discord)'>გახსენით ბილეთი Discord_ზე </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(დააჭირეთ რომ გახსნათ GitHub)'>ან შექმენით ახალი პრობლემის პოსტი GitHub_ზე.
- ''
# ქვე-ბრძანებების ფორმატი რომელიც არის ნაჩვენები მთავარი ბრძანების გაცემის დროს.
subcommands: '<suggest-subcommand><hover:show_text:''<gray>მხოლოდ მოთამაშეები: </gray><only-players><br><gray>აუცილებელია კონსოლი: </gray><only-console><br><gray>უფლება: </gray><white><permission><br><gray>სხვა-სახელი: </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <ქვე-ბრძანება> <white><description></hover></suggest-subcommand>'
# ფორმატი კი_სთვის (სტიჩკა) და არა_სთვის (ჯვარი) (გამოიყენება ქვე-ბრძანებებში)
tick: '<green>✔</green>'
cross: '<red>✗</red>'
# თარგმანი '/sonar reload' სთვის
reload:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "ხელახლა იტვირთება ყველა კონფიკურაცია"
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე ხელახლა ჩატვირთავს სონარს
start: '<prefix>ხელახლა იტვირთება Sonar...'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა Sonar_ი დაამთავრებს ხელახლა ჩატვირთვას
finish: '<prefix><green>ხელახლა ჩატვირთვა წარმატებით მოხერხდა <gray>(<time-taken>ms)'
# თარგმანი '/sonar verbose' სთვის
verbose:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "ჩართე ან გამორთე Sonar_ის დეტალური ინფორმაციის ჩვენება"
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა მოთამაშე ბრძანებს Sonarს დეტალური ინფორმაციის ჩვენებას
subscribe: '<prefix>თქვენ ამიერიდან ხედავთ Sonar_ის დეტალურ ინფორმაციას.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ბრძანებს რომ აღარ დაინახოს დეტალური ინფორმაცია
unsubscribe: '<prefix>თქვენ ვეღარ იხილავთ Sonar_ის დეტალურ ინფორმაციას.'
# თარგმანი '/sonar notify' სთვის
notify:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "ჩართე ან გამორთე შეტევის ცნობები"
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა მოთამაშე ბრძანებს რომ დაინახოს შეტევების ცნობები
subscribe: '<prefix>თქვენ ამიერიდან ხედავთ Sonarს შეტევის ცნობებს.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა მოთამაშე ბრძანებს რომ აღარ დაინახოს შეტევების ცნობები
unsubscribe: '<prefix>თქვენ ვეღარ იხილავთ Sonarს შეტევის ცნობებს.'
# თარგმანი '/sonar blacklist' სთვის
blacklist:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "შეცვალე აკრძალული (დაბლოკილი) IP მისამართები"
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე ცდის შავი ნუსხის გასუფთავებას თუმცა იგი ცარიელია.
empty: '<prefix>შავი ნუსხა ამჟამად ცარიელია. ასე რომ არცერთი IP მისამართი არ მოშორებულა.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე გაასუფთავებს შავ ნუსხას.
clear: '<prefix>თქვენ წარმატებით გაასუფთავეთ <amount> IP მისამართ(ებ)ი შავი ნუსხიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე შეამოწმებს შავი ნუსხის ზომას.
size: '<prefix>ამჟამად შავი ნუსხა შეიცავს <amount> IP მისამართ(ებ)ს.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოაშორებს IP მისამართს შავი ნუსხიდან.
remove: '<prefix>თქვენ წარმატებით მოაშორეთ <ip> შავი ნუსხიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში.
add: '<prefix>თქვენ წარმატებით დაამატეთ <ip> შავ ნუსხაში.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ ეს მისამართი ვერიფიცირებულია
add-warning: '<prefix><red>გაფრთხილება: <white><ip> ამჟამად არის თეთრ ნუსხაში. გთხოვთ მოაშოროთ ეს IP მისამართი თეთრი სიიდან რათა აიცილოთ თავიდან პოტენციური პრობლემები.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ იგი უკვე არის იქ.
ip-duplicate: '<prefix>ეს IP მისამართი ისედაც შავ ნუსხაშია.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოაშორებს IP მისამართს შავი ნუსხიდან მაგრამ იგი ისედაც არ არის იქ.
ip-not-found: '<prefix>ეს IP მისამართი ისედაც არ არის შავ ნუსხაში.'
# თარგმანი '/sonar verified' სთვის
verified:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "Manage verified IP მისამართები"
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე ცდის გაასუფთავოს ნუსხა შემოწმებული მოთამაშეებისა თუმცა იგი ისედაც ცარიელია.
empty: '<prefix>ნუსხა შემოწმებული მოთამაშეებისა ისედაც ცარიელია. ასე რომ არ მოშორებულა არცერთი IP მისამართი იქიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე გაასუფთავებს ნუსხას შემოწმებული მოთამაშეებისა.
clear: '<prefix>თქვენ წარმატებით გაასუფთავეთ <amount> მოთამაშე შემოწმებულების ნუსხიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე შეამოწმებს სიდიდეს შემოწმებული მოთამაშეების ნუსხისა.
size: '<prefix>ამჟამად <amount> მოთამაშეა შემოწმებული.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოშლის IP მისამართს შემოწმებულების სიიდან.
remove: '<prefix>წარმატებით მოშორდა <ip>.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შემოწმებულების ნუსხაში.
add: '<prefix>Successfully verified <ip>.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე ცდის რომ დაამატოს IP/UUID შემოწმებულების ნუსხაში რაც უკვე დამატებულია.
already: '<prefix>IP მისამართი/UUID რაც მოგვაწოდეთ ისედაც დამატებულია.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოშლის IP მისამართს შემოწმებულების ნუსხიდან მაგრამ ის იქ არ არის.
ip-not-found: '<prefix>IP მისამართი ისედაც არ არის შემოწმებულების ნუსხაში.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე შეამოწმებს მოთამაშის შემოწმების ისტორიას.
history: '<prefix><yellow>მანამდელი UUIDები <ip>_ის:'
history-entry: ' <gray>▪ <white><uuid>'
# თარგმანი '/sonar statistics' სთვის
statistics:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "აჩვენე სერვერის ამჟამინდელი სტატისტიკები"
# ინფორმაციული მესიჯი რაც არის ნაჩვენები ყველაფრის ზემოთ როცა ნახულობთ სტატისტიკებს.
header: '<prefix>ვაჩვენებთ <statistics-type> სტატისტიკას:'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა მოთამაშე ცდის იხილოს უცნობი სტატისტიკა.
unknown-type: '<prefix><red>არასწორი სტატისტიკის ტიპი! ხელმისაწვდომი სტატისტიკები: <gray><statistics>'
# ფორმატი სტატისტიკებისა
general:
- ' <dark_aqua>▪ <gray>შემოწმებული IP მისამართები: <white><verified>'
- ' <dark_aqua>▪ <gray>მოწმდება IP მისამართები: <white><verifying>'
- ' <dark_aqua>▪ <gray>შავ ნუსხაში შევიდა IP მისამართები: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>ამჟამად რიგში ჩაყენებული შემოსვლები: <white><queued>'
- ' <dark_aqua>▪ <gray>ჯამში არა-unique შემოსვლები: <white><total-joins>'
- ' <dark_aqua>▪ <gray>ჯამში შემოწმების მცდელობები: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>ჯამში წარუმატებელი შემოწმებები: <white><total-failed>'
- ' <dark_aqua>▪ <gray>სერვერის ჩართვის დრო: <white><server-uptime>'
# ფორმატი პროცესორის სტატისტიკისა
cpu:
- ' <dark_aqua>▪ <gray>პროცესის CPU გამოყენება ზუსტად ახლა: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>სისტემის CPU გამოყენება ზუსტად ახლა: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>თითო-ბირთვის პროცესის CPU გამოყენება: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>თითო-ბირთვის სისტემის CPU გამოყენება: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>ჯამში ვირტუალური CPU რაოდენობა: <white><virtual-core-count>'
# ფორმატი სწრაფი მეხსიერებისა
memory:
- ' <dark_aqua>▪ <gray>ჯამში თავისუფალი სწრაფი მეხსიერება:<white><free-memory>'
- ' <dark_aqua>▪ <gray>ჯამში გამოყენებული სწრაფი მეხსიერება:<white><used-memory>'
- ' <dark_aqua>▪ <gray>ჯამში მაქსიმუმი სწრაფი მეხსიერება:<white><max-memory>'
- ' <dark_aqua>▪ <gray>ჯამში გამოყოფილი სწრაფი მეხსიერება:<white><total-memory>'
# ფორმატი ქსელის სტატისტიკისა
network:
- ' <dark_aqua>▪ <gray>ამჟამინდელი მომდინარე გამოყენებული ქსელის გამოყენება: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>ამჟამინდელი გამდინარე გამოყენებული ქსელის გამოყენება: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>ჯამში მომდინარე გამოყენებული ქსელის გამოყენება: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>ჯამში გამდინარე გამოყენებული ქსელის გამოყენება: <white><incoming-traffic-ttl>'
# თარგმანი '/sonar dump' სთვის
dump:
# აღწერა ამ ქვე-ბრძანებისთვის
description: "Print developer information"
# ფორმატი ინფორმაციისთვის რაც კონსოლს ეგზავნება
log: "გენერირდა: <dumped-json-data>"
# თარგმანი ყველა მესიჯისთვის შემოწმების პროცესის დროს
verification:
# თარგმანი ყველა დეტალური ინფორმაციის მესიჯებისა
logs:
# მესიჯი ჩაწერილი კონსოლში, როცა ახალი მოთამაშე შემოდის
connection: '<username><ip> (<protocol>) დაკავშირდა.'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე წარუმატებლად ასრულებს შემოწმებას
failed: '<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შავ ნუსხაშია
blacklisted: '<ip> (<protocol>) ბევრი წარუმატებელი შემოწმების გამო მოთამაშე შავ ნუსხაში შევიდა'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შემოწმდა წარმატებულად
successful: '<username> წარმატებულად შემოწმდა (<time-taken>s!).'
# მესიჯი რომელიც გაეგზავნება მოთამაშეს როცა მათ უნდა დაწერონ CAPTCHA
captcha:
# მესიჯი რომელიც გაეგზავნება მოთამაშეს როცა მათ უნდა მიუთითონ CAPTCHA ს პასუხი
enter-code: '<prefix><green>გთხოვთ დაწეროთ ჩატში ის კოდი რომელიც არის ნაჩვენები რუკაზე.'
# მესიჯი რომელიც გაეგზავნება მოთამაშეს როცა ისინი ჩაწერენ არასწორ პასუხს ჩატში
incorrect: '<prefix><red>თქვენ მიუთითეთ არასწორი კოდი. გთხოვთ ხელახლა ცადოთ.'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა მაგრამ CAPTCHA არ არის მზად
currently-preparing:
- '<header>'
- <yellow>ანტიბოტი ჯერ არ მომზადებულა.
- <gray>გთხოვთ დაიცადოთ რამოდენიმე წამი სანამ ხელახლა შემოხვალთ.
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე ხელახლა შემოდის ძალიან სწრაფად შემოწმების დროს.
too-fast-reconnect:
- '<header>'
- <gold>თქვენ კავშირდებით ხელახლა ძალიან სწრაფად.
- <gray>გთხოვთ დაიცადოთ რამოდენიმე წამი სანამ ხელახლა ცდით შემოსვლას.
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე ცდის შემოსვლას თუმცა იგი უკვე მოწმდება
already-verifying:
- '<header>'
- <red>თქვენი IP მისამართი ამჟამად უკვე მოწმდება.
- <red>გთხოვთ დაიცადოთ რამოდენიმე წამი სანამ ხელახლა ცდით შემოსვლას.
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა თუმცა იგი უკვე არის შემოწმების რიგში
already-queued:
- '<header>'
- <red>თქვენი IP მისამართი უკვე არის რიგში შემოწმებისთვის.
- <red>გთხოვთ დაიცადოთ რამოდენიმე წუთი სანამ ხელახლა ცდით შემოსვლას.
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა თუმცა დროებით დაბლოკილია
blacklisted:
- '<header>'
- <red>თქვენ ამჟამად დაბლოკილი ხართ სერვერიდან.
- <red>გთხოვთ დაიცადოთ რამოდენიმე წუთი რომ ისევ შეგეძლოტ სერვერზე შემოსვლა.
- <gold>შეცდომა? <gray><support-url>
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა არასწორი სახელით
invalid-username:
- '<header>'
- <red>შენი სახელი შეიცავს არასწორ კომპონენტებს.
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოვა დაბლოკილი ვერსიით
blacklisted-protocol:
- '<header>'
- <red>შენ იყენებ ვერსიას რომელიც არ არის დაშვებული სერვერზე.
- <gold>გჭირდება დახმარება შესვლაში? <gray><support-url>
- '<footer>'
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე შემოწმდა წარმატებით
success:
- '<header>'
- <green>თქვენ წარმატებით გაიარეთ შემოწმება.
- <white>თქვენ უკვე შეგიძლიათ შეხვიდეთ სერვერზე.
# გავარდნის მესიჯი რომელიც არის ნაჩვენები როცა ვინმე წარუმატებლად გაივლის შემოწმებას
failed:
- '<header>'
- <red>თქვენ ვერ გაიარეთ შემოწმება.
- <gray>გთხოვთ დაიცადოთ რამოდენიმე წამი სანამ ხელახლა ცდით შემოსვლას.
- <gold>გჭირდებათ დახმარება შესვლაში? <gray><support-url>
- '<footer>'
# თარგმანი Sonar_ის actionbar დეტალური ინფორმაციის მესიჯებისთვის
verbose:
layout:
# ჩვენება Sonar_ის actionbar დეტალური ინფორმაციისთვის
normal: '<prefix><gray>კთწ <white><connections-per-second> <dark_aqua>╺ <gray>შემოსვლა/წ <white><logins-per-second> <dark_aqua>╺ <gray>მოწმდება <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Layout for Sonar's actionbar verbose during an attack
attack: '<prefix><gray>კთწ <white><connections-per-second> <dark_aqua>╺ <gray>შემოსვლა/წ <white><logins-per-second> <dark_aqua>╺ <gray>რიგი <white><queued> <dark_aqua>╺ <gray>მოწმდება <white><verifying> <dark_aqua>╺ <gray>ხანგრძლივობა <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animation for the action bar
# Alternatives:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# თარგმანი Sonar's შეტევის ცნობების მესიჯებისთვის
notifications:
# ჩვენება Sonar_ის შეტევის ცნობების სათაურისთვის
title: <yellow><b>Sonar<reset>
# ჩვენება Sonar_ის შეტევის ცნობების ქვე-სათაურისთვის
subtitle: <gray>დაფიქსირდა შეტევა
# ჩვენება Sonar_ის შეტევის ცნობების ჩატისთვის
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>დაფიქსირდა შეტევა.
- ' <dark_aqua>▪ <gray>შემოსვლა თითო წამში: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>მოწმდება: <white><verifying>'
- ' <dark_aqua>▪ <gray>რიგის ზომა: <white><queued>'
- ''
- <gray>იხილეთ ინფორმაცია: <white><click:run_command:'/sonar verbose'><hover:show_text:'(დააჭირეთ შესასრულებლად)'>/sonar verbose</hover></click><gray> ან <white><click:run_command:'/sonar stats'><hover:show_text:'(დააჭირეთ შესასრულებლად)'>/sonar stats</hover></click><gray>.
- ''
@@ -0,0 +1,295 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# 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
#
# Placeholder for every '<prefix>' in this configuration file
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Placeholder for every '<support-url>' in this configuration file
support-url: jhttps://jonesdev.xyz/discord/
# Placeholder for every '<header>' in this configuration file
header:
- <b><yellow>Sonar</b><reset>
- ''
# Placeholder for every '<footer>' in this configuration file
footer:
- <gray>Jeśli uważasz, że jest to błąd, skontaktuj się z administratorem.
# Disconnect message that is shown when someone joins, but there are too many online players with their IP address
too-many-online-per-ip:
- <header>
- <red>Jest zbyt wielu graczy online z Twoim adresem IP.
- <footer>
# Translations for commands
commands:
# Message shown when a player tries running /sonar without permission
no-permission: <prefix><red>Nie masz uprawnień do wykonania tego polecenia.
# Message shown when someone uses a command incorrectly
incorrect-usage: '<prefix><red>Użyj: /sonar <subcommand-usage>'
# Message shown when someone provides an invalid IP address (Invalid format)
invalid-ip-address: <prefix>Podany adres IP wydaje się być nieprawidłowy.
# Message shown when the console runs a command that is player-only
player-only: <prefix><red>Polecenie te może wykonać tylko jako gracz.
# Message shown when a player runs a command that is console-only
console-only: <prefix><red>Ze względów bezpieczeństwa polecenie to można wykonać tylko za pośrednictwem konsoli.
# Message shown when a player does not have permission to execute a certain subcommand
subcommand-no-permission: <prefix><red>Nie masz uprawnień do wykonania tej podkomendy. <gray>(<permission>)
# Translations for '/sonar'
main:
# Informational message that is shown above everything when running the main command
header:
- <yellow>Uruchomiony <b>Sonar</b> <version> na <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year>
Jones Development and Sonar Contributors</click>
- ''
- <gray>Potrzebujesz pomocy lub masz pytania?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Kliknij aby otworzyć Discord)'>Otwórz zgłoszenie na Discordzie </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Kliknij aby otworzyć GitHub)'>lub otwórz nowe zgłoszenie w serwisie GitHub.
- ''
# Formatting of the list of subcommands shown when running the main command
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Only players: </gray><only-players><br><gray>Require
console: </gray><only-console><br><gray>Permission: </gray><white><permission><br><gray>Aliases:
</gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Formatting for yes (tick) and no (cross) values in the subcommands hover text
tick: <green>✔</green>
cross: <red>✗</red>
# Translations for '/sonar reload'
reload:
# Description for this subcommand
description: Przeładuj wszystkie konfiguracje
# Message shown when someone starts reloading Sonar
start: <prefix>Przeładowanie sonaru...
# Message shown when Sonar has finished reloading
finish: <prefix><green>Pomyślne przeładowanie <gray>(<time-taken>ms)
# Translations for '/sonar verbose'
verbose:
# Description for this subcommand
description: Włączanie lub wyłączanie funkcji Sonar verbose
# Message shown when a player subscribes to Sonar verbose
subscribe: <prefix>Wyświetlana jest teraz pełna wersja programu Sonar.
# Message shown when a player unsubscribes from Sonar verbose
unsubscribe: <prefix>Sonar nie jest już wyświetlany w trybie verbose.
# Translations for '/sonar notify'
notify:
# Description for this subcommand
description: Włączanie lub wyłączanie powiadomień o atakach
# Message shown when a player subscribes to Sonar attack notifications
subscribe: <prefix>Wyświetlane są teraz powiadomienia o atakach Sonar.
# Message shown when a player unsubscribes from Sonar attack notifications
unsubscribe: <prefix>Powiadomienia o atakach Sonar nie są już wyświetlane.
# Translations for '/sonar blacklist'
blacklist:
# Description for this subcommand
description: Zarządzanie adresami IP z czarnej listy
# Message shown when someone tries clearing the blacklist but is empty
empty: <prefix>Czarna lista jest obecnie pusta. W związku z tym żadne adresy IP nie zostały usunięte z czarnej listy.
# Message shown when someone clears the blacklist
clear: <prefix>Udało Ci się usunąć łącznie <amount> adresów IP z czarnej listy.
# Message shown when someone checks the size of the blacklist
size: <prefix>Czarna lista zawiera obecnie <amount> adresów IP.
# Message shown when someone removes an IP address from the blacklist
remove: <prefix>Pomyślnie usunięto <ip> z czarnej listy.
# Message shown when someone adds an IP address to the blacklist
add: <prefix>Pomyślnie dodano <ip> do czarnej listy.
# Message shown when someone adds an IP address to the blacklist that is verified
add-warning: '<prefix><red>Ostrzeżenie: <white><ip> znajduje się obecnie na białej liście. Rozważ usunięcie adresu IP z listy zweryfikowanych graczy, aby uniknąć potencjalnych problemów.'
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
ip-duplicate: <prefix>Podany adres IP znajduje się już na czarnej liście.
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
ip-not-found: <prefix>Podany adres IP nie znajduje się na czarnej liście.
# Translations for '/sonar verified'
verified:
# Description for this subcommand
description: Zarządzanie zweryfikowanymi adresami IP
# Message shown when someone tries clearing the list of verified players but is empty
empty: <prefix>Lista zweryfikowanych graczy jest obecnie pusta. W związku z tym żaden gracz nie został niezweryfikowany.
# Message shown when someone clears the list of verified players
clear: <prefix>Udało ci się zweryfikować łącznie <amount> unikalnych graczy.
# Message shown when someone checks the size of the list of verified players
size: <prefix>Obecnie zweryfikowanych jest <amount> unikalnych graczy.
# Message shown when someone un-verifies an IP address
remove: <prefix>Pomyślnie niezweryfikowany <ip>.
# Message shown when someone verifies an IP address
add: <prefix>Pomyślnie zweryfikowany <ip>.
# Message shown when someone tries to verify an IP address/UUID that are already verified
already: <prefix>Podany adres IP i identyfikator UUID zostały już zweryfikowane.
# Message shown when someone un-verifies an IP address, but it is not verified
ip-not-found: <prefix>Podany adres IP nie został zweryfikowany.
# Message shown when someone looks up the verification history of an IP address
history: '<prefix><yellow>Poprzednie identyfikatory UUID dla <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Translations for '/sonar statistics'
statistics:
# Description for this subcommand
description: Wyświetlanie statystyk sesji tego serwera
# Informational message that is shown above everything when viewing the statistics
header: '<prefix>Wyświetlanie <statistics-type> statystyki dla tej sesji:'
# Message shown when a player tries viewing an unknown statistic
unknown-type: '<prefix><red>Nieznany typ statystyk! Dostępne statystyki: <gray><statistics>'
# Format of the general statistics message
general:
- ' <dark_aqua>▪ <gray>Zweryfikowane adresy IP: <white><verified>'
- ' <dark_aqua>▪ <gray>Weryfikacja adresów IP: <white><verifying>'
- ' <dark_aqua>▪ <gray>Adresy IP na czarnej liście: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Aktualnie oczekujące logowania: <white><queued>'
- ' <dark_aqua>▪ <gray>Łączna liczba nieunikalnych połączeń: <white><total-joins>'
- ' <dark_aqua>▪ <gray>Łączna liczba prób weryfikacji: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Łączna liczba nieudanych weryfikacji: <white><total-failed>'
- ' <dark_aqua>▪ <gray>Czas pracy serwera: <white><server-uptime>'
# Format of the CPU statistics message
cpu:
- ' <dark_aqua>▪ <gray>Wykorzystanie procesora CPU w tej chwili: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>Obecne użycie procesora systemowego: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Użycie procesora na rdzeń: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Użycie procesora na rdzeń systemu: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Całkowita liczba wirtualnych procesorów: <white><virtual-core-count>'
# Format of the memory statistics message
memory:
- ' <dark_aqua>▪ <gray>Całkowita ilość wolnej pamięci: <white><free-memory>'
- ' <dark_aqua>▪ <gray>Całkowita wykorzystana pamięć: <white><used-memory>'
- ' <dark_aqua>▪ <gray>Całkowita maksymalna pamięć: <white><max-memory>'
- ' <dark_aqua>▪ <gray>Całkowita przydzielona pamięć: <white><total-memory>'
# Format of the network statistics message
network:
- ' <dark_aqua>▪ <gray>Aktualnie używana przychodząca przepustowość: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Aktualnie używana przepustowość wychodząca: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Całkowita wykorzystana przepustowość przychodząca: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Całkowita wykorzystana przepustowość wychodząca: <white><incoming-traffic-ttl>'
# Translations for '/sonar dump'
dump:
# Description for this subcommand
description: Drukowanie informacji o deweloperze
# Format of the information sent to console
log: 'Wygenerowany zrzut: <dumped-json-data>'
# Translations for all messages during the verification process
verification:
# Translations for all debug messages during the verification
logs:
# Message logged to console whenever a new player joins the server
connection: <username><ip> (<protocol>) połączył się.
# Message logged to console whenever a player fails verification
failed: '<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
# Message logged to console whenever a player is blacklisted
blacklisted: <ip> (<protocol>) został umieszczony na czarnej liście z powodu zbyt wielu nieudanych prób
# Message logged to console whenever a player is verified
successful: <username> został pomyślnie zweryfikowany (<time-taken>s!).
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
captcha:
# Message shown to the player when they have to enter the answer to the captcha
enter-code: <prefix><green>Wprowadź na czacie kod wyświetlany na mapie.
# Message shown to the player when they enter the wrong answer in chat
incorrect: <prefix><red>Wprowadzono nieprawidłowy kod. Spróbuj ponownie.
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
currently-preparing:
- <header>
- <yellow>Twoje dane antybotowe nie zostały jeszcze przygotowane.
- <gray>Odczekaj kilka sekund przed ponowną próbą weryfikacji.
- <footer>
# Disconnect message that is shown when someone rejoins too fast during verification
too-fast-reconnect:
- <header>
- <gold>Połączenie zostało nawiązane zbyt szybko, spróbuj ponownie.
- <gray>Odczekaj kilka sekund przed ponowną próbą weryfikacji.
- <footer>
# Disconnect message that is shown when someone joins but is already verifying
already-verifying:
- <header>
- <red>Twój adres IP jest obecnie weryfikowany.
- <red>Odczekaj kilka sekund przed ponowną próbą weryfikacji.
- <footer>
# Disconnect message that is shown when someone joins but is already queued for verification
already-queued:
- <header>
- <red>Twój adres IP znajduje się obecnie w kolejce do weryfikacji.
- <red>Odczekaj kilka minut przed ponowną próbą weryfikacji.
- <footer>
# Disconnect message that is shown when someone joins but is temporarily blacklisted
blacklisted:
- <header>
- <red>Obecnie nie możesz wejść na serwer.
- <red>Odczekaj kilka minut, aby móc ponownie dołączyć do serwera.
- <footer>
# Disconnect message that is shown when someone joins with an invalid username
invalid-username:
- <header>
- <red>Twoja nazwa użytkownika zawiera nieprawidłowe znaki.
- <footer>
# Disconnect message that is shown when someone joins with a blacklisted version
blacklisted-protocol:
- <header>
- <red>Używasz wersji, która nie jest dozwolona na naszym serwerze.
- <gold>Potrzebujesz pomocy przy logowaniu? <gray><support-url>
- <footer>
# Disconnect message that is shown when someone verifies successfully
success:
- <header>
- <green>Pomyślnie przeszedłeś weryfikację.
- <white>Możesz teraz grać na serwerze po ponownym połączeniu.
# Disconnect message that is shown when someone fails verification
failed:
- <header>
- <red>Weryfikacja nie powiodła się.
- <gray>Odczekaj kilka sekund przed ponowną próbą weryfikacji.
- <gold>Potrzebujesz pomocy przy logowaniu? <gray><support-url>
- <footer>
# Translations for Sonar's actionbar verbose messages
verbose:
layout:
# Layout for Sonar's actionbar verbose
normal: <prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s
<white><logins-per-second> <dark_aqua>╺ <gray>Verifying <white><verifying> <dark_aqua>|
<green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>
# Layout for Sonar's actionbar verbose during an attack
attack: <prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s
<white><logins-per-second> <dark_aqua>╺ <gray>Queue <white><queued> <dark_aqua>╺
<gray>Verifying <white><verifying> <dark_aqua>╺ <gray>Duration <white><attack-duration>
<dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>
# Animation for the action bar
# Alternatives:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Translations for Sonar's attack notification messages
notifications:
# Layout for Sonar's attack notification title
title: <yellow><b>Sonar<reset>
# Layout for Sonar's attack notification subtitle
subtitle: <gray>Wykryto atak botów
# Layout for Sonar's attack notification in chat
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Wykryto atak botów.
- ' <dark_aqua>▪ <gray>Logowania na sekundę: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Weryfikacja graczy: <white><verifying>'
- ' <dark_aqua>▪ <gray>Gracze w kolejce: <white><queued>'
- ''
- <gray>Wyświetl więcej informacji za pomocą <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar stats'><hover:show_text:'(Kliknij, aby uruchomić)'>/sonar stats</hover></click><gray>.
- ''
@@ -191,12 +191,12 @@ verification:
# CAPTCHA'yı girmeleri gerektiğinde oyuncuya gösterilen mesajlar
captcha:
# Captcha'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
# CAPTCHA'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
enter-code: '<prefix><green>Haritada gösterilen kodu lütfen sohbete girin.'
# Message that is shown to the player when they enter the wrong answer in chat
incorrect: '<prefix><red>Yanlış kodu girdiniz. Lütfen tekrar deneyin.'
# Captcha henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
# CAPTCHA henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
currently-preparing:
- '<header>'
- <yellow>Anti-bot verileriniz henüz hazır değil.
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Butuh bantuan atau punya pertanyaan? https://jonesdev.xyz/discord
# Kode sumber: https://github.com/jonesdevelopment/sonar
# Hak cipta Jones Development dan Kontributor Sonar, dilisensikan di bawah GPL-3.0
#
# Notifikasi serangan bot juga dapat dikirim ke server Discord Anda menggunakan webhook
webhook:
# URL dari webhook Discord (Atur ini ke '' untuk menonaktifkan webhook)
url: ''
# Nama pengguna pengirim webhook Discord
username: Sonar
# URL avatar pengirim webhook Discord (Atur ini ke '' untuk menonaktifkan)
avatar-url: ''
# Konten pesan webhook Discord (Atur ini ke '' untuk menonaktifkan)
# Anda dapat menggunakan ini untuk misalnya memberi ping ke anggota staf menggunakan <@userId>
# Jika Anda ingin memberi ping ke peran, Anda harus menggunakan <@&roleId>
content: ''
# Pesan embed webhook Discord yang dikirim ketika serangan telah berhenti
embed:
# Pesan footer kecil dari embed webhook Discord
footer:
# Konten pesan footer dari embed webhook Discord
text: © Jones Development dan Kontributor Sonar
# URL ikon pesan footer dari embed webhook Discord
icon-url: ''
# Judul dari embed webhook Discord
title: '✅ Serangan berhasil diatasi'
# URL yang dapat diklik dari judul embed webhook Discord
title-url: ''
# Deskripsi (konten) dari embed webhook Discord
description:
- Serangan pada server Anda telah berhasil diatasi.
- ''
- 'Mulai serangan: <t:<start-timestamp>:T>'
- 'Akhir serangan: <t:<end-timestamp>:T>'
- 'Durasi serangan: <attack-duration>'
- ''
- 'Penggunaan CPU proses puncak selama serangan: <peak-cpu>%'
- 'Penggunaan memori proses puncak selama serangan: <peak-memory>'
- 'Bot per detik puncak selama serangan: <peak-bps>'
- ''
- 'Alamat IP yang masuk daftar hitam selama serangan: <total-blacklisted>'
- 'Verifikasi yang gagal selama serangan: <total-failed>'
- 'Verifikasi yang berhasil selama serangan: <total-success>'
# Warna RGB dari embed webhook Discord
# Pemilih warna: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# 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
#
# Bot attack notifications can also be sent to your Discord server using webhooks
webhook:
# URL of the Discord webhook (Set this to '' to disable webhooks)
url: ''
# Username of the Discord webhook sender
username: Sonar
# URL to the avatar of the Discord webhook sender (Set this to '' to disable)
avatar-url: ''
# Content of the Discord webhook message (Set this to '' to disable)
# You can use this to e.g. ping staff members using <@userId>
# If you want to ping roles, you will need to use <@&roleId>
content: ''
# Embed Discord webhook message that is sent when an attack has stopped
embed:
# Small footer message of the Discord webhook embed
footer:
# Content of the footer message of the Discord webhook embed
text: © Jones Development and Sonar Contributors
# URL of the footer message icon of the Discord webhook embed
icon-url: ''
# Title of the Discord webhook embed
title: ':white_check_mark: Atak został złagodzony'
# Clickable URL of the title of the Discord webhook embed
title-url: ''
# Description (content) of the Discord webhook embed
description:
- Atak na serwer został złagodzony.
- ''
- 'Początek ataku: <t:<start-timestamp>:T>'
- 'Koniec ataku: <t:<end-timestamp>:T>'
- 'Czas trwania ataku: <attack-duration>'
- ''
- 'Szczytowe użycie procesora przez proces podczas ataku: <peak-cpu>%'
- 'Szczytowe użycie pamięci procesu podczas ataku: <peak-memory>'
- 'Szczytowa liczba botów na sekundę podczas ataku: <peak-bps>'
- ''
- 'Adresy IP na czarnej liście podczas ataku: <total-blacklisted>'
- 'Nieudane weryfikacje podczas ataku: <total-failed>'
- 'Pomyślne weryfikacje podczas ataku: <total-success>'
# RGB colors of the Discord webhook embed
# Color picker: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -28,19 +28,16 @@ import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.velocity.fallback.FallbackVelocityInjector;
import java.util.UUID;
@Getter
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
public static SonarVelocity INSTANCE;
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
super(plugin, SonarPlatform.VELOCITY, plugin.getDataDirectory().toFile(),
new VelocityLibraryManager<>(plugin, plugin.getLogger(),
plugin.getDataDirectory(), plugin.getServer().getPluginManager()));
INSTANCE = this;
}
/**
@@ -54,6 +51,13 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
return getPlugin().getServer().getPlayer(uniqueId).orElse(null);
}
@Override
@Deprecated
public @NotNull Audience sender(final @NotNull Object object) {
// We don't have to do this on Velocity since all command senders are also audiences.
throw new UnsupportedOperationException();
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -90,12 +94,16 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject(getPlugin().getServer());
FallbackVelocityInjector.inject(getPlugin().getServer());
}
@Override
@@ -1,60 +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.velocity.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
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 com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
@RequiredArgsConstructor
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
private static final MethodHandle INIT_CHANNEL_METHOD;
static {
try {
INIT_CHANNEL_METHOD = MethodHandles.privateLookupIn(ChannelInitializer.class, MethodHandles.lookup())
.findVirtual(ChannelInitializer.class, "initChannel", MethodType.methodType(void.class, Channel.class));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
@Override
protected void initChannel(final Channel channel) throws Exception {
// Invoke the original method
try {
INIT_CHANNEL_METHOD.invokeExact(originalChannelInitializer, channel);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
if (channel.pipeline().get(MINECRAFT_DECODER) != null) {
channel.pipeline().addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
}
@@ -20,38 +20,37 @@ package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.*;
import static com.velocitypowered.proxy.network.Connections.HANDLER;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
public FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
final class FallbackVelocityInboundHandler extends FallbackInboundHandlerAdapter {
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any handshake packet by the client
if (listenForPackets) {
if (msg instanceof HandshakePacket handshake) {
handleHandshake(handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
}
// Intercept any server login packet by the client
else if (msg instanceof ServerLoginPacket serverLogin) {
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
handleLogin(ctx, serverLogin, serverLogin.getUsername(), socketAddress,
MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
return;
if (msg instanceof HandshakePacket handshake) {
handleHandshake(ctx.channel(), handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
// We don't care about server pings; remove the handler
if (handshake.getNextStatus() == 1) {
ctx.channel().pipeline().remove(this);
}
}
// Intercept any server login packet by the client
else if (msg instanceof ServerLoginPacket serverLogin) {
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) ctx.channel().pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> ctx.fireChannelRead(msg), serverLogin.getUsername(), socketAddress);
return;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.network.ServerChannelInitializerHolder;
@@ -25,11 +24,15 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackInjectionHelper {
public class FallbackVelocityInjector {
private final Field CONNECTION_MANAGER_FIELD;
private final Field SERVER_CHANNEL_INITIALIZER_FIELD;
@@ -45,16 +48,18 @@ public class FallbackInjectionHelper {
}
}
public void inject(final ProxyServer proxyServer) {
public void inject(final Object proxyServer) {
try {
final ConnectionManager connectionManager = (ConnectionManager) CONNECTION_MANAGER_FIELD.get(proxyServer);
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = connectionManager.serverChannelInitializer.get();
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(MINECRAFT_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackVelocityInboundHandler()));
// Replace the original channel initializer
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), fallbackInitializer);
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), injectedInitializer);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}