mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a472d3aa | ||
|
|
76f0f4abda | ||
|
|
720e1cb1cb | ||
|
|
a98d9337c7 | ||
|
|
5158111edf | ||
|
|
2ab829b43e | ||
|
|
04bbfde6b6 | ||
|
|
ef8abcec46 | ||
|
|
1fb38ec8d4 | ||
|
|
cc5e958e2a | ||
|
|
1437204a15 | ||
|
|
c96d733920 | ||
|
|
c561b3eeaf | ||
|
|
2310864816 | ||
|
|
55030c5073 | ||
|
|
18bb9557b1 | ||
|
|
5d02050815 | ||
|
|
5914b23ae5 | ||
|
|
7ce56a79ab | ||
|
|
7b17ba7e53 | ||
|
|
8268cda660 | ||
|
|
89d0c7346f | ||
|
|
512faa956e | ||
|
|
1f6781aaa3 | ||
|
|
bf40333de5 | ||
|
|
31b961b577 | ||
|
|
e4821781fa | ||
|
|
bdcb7d28a7 | ||
|
|
433fab10f1 | ||
|
|
014b956f9e | ||
|
|
4be1a970d0 | ||
|
|
bea1d292ee | ||
|
|
468a11e0be | ||
|
|
cf2fce9c29 |
No files matched your search
+1
-5
@@ -42,11 +42,7 @@
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
|
||||
- Sonar queues new player logins to prevent spam login attacks
|
||||
- Sonar checks the handshake packets for legitimacy
|
||||
- Protective components only on BungeeCord:
|
||||
- Sonar validates the login/server ping packets
|
||||
- Sonar handles all server pings asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
- Sonar makes sure some packets cannot be duplicated illegitimately
|
||||
|
||||
### Fallback
|
||||
|
||||
|
||||
+19
-11
@@ -21,8 +21,8 @@ versioner {
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
|
||||
}
|
||||
|
||||
apply(plugin = "java")
|
||||
@@ -43,10 +43,12 @@ allprojects {
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:$adventureVersion")
|
||||
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
|
||||
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.104.Final") // netty
|
||||
// Library/dependency loading
|
||||
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +71,7 @@ tasks {
|
||||
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("Sonar.jar")
|
||||
archiveFileName.set("${rootProject.name}.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
@@ -80,17 +82,23 @@ tasks {
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
|
||||
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
exclude("net.kyori.adventure.audience.**")
|
||||
exclude("net.kyori.adventure.audience.Audience")
|
||||
exclude("net.kyori.adventure.title.*")
|
||||
}
|
||||
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
|
||||
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
|
||||
// Relocate dynamically loaded libraries
|
||||
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
// We want to load our own Gson dynamically
|
||||
exclude("com/google/gson/**")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/versions/**")
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.14
|
||||
version=2.0.18
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "sonar"
|
||||
rootProject.name = "Sonar"
|
||||
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
val path = "sonar-$it"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
repositories {
|
||||
maven(url = "https://jitpack.io/") // simple-yaml
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
@@ -28,10 +30,13 @@ import xyz.jonesdev.sonar.api.event.SonarEventManager;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
@@ -41,6 +46,12 @@ public interface Sonar {
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* Since we want to use Adventure on every server platform,
|
||||
* we have to use their platform module to support BungeeCord and Bukkit
|
||||
*/
|
||||
Map<String, Audience> AUDIENCES = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@@ -61,14 +72,24 @@ public interface Sonar {
|
||||
|
||||
@NotNull Verbose getVerboseHandler();
|
||||
|
||||
@NotNull Notification getNotificationHandler();
|
||||
|
||||
@NotNull SystemTimer getLaunchTimer();
|
||||
|
||||
@NotNull LibraryManager getLibraryManager();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setVerboseHandler(final @NotNull Verbose verboseHandler);
|
||||
|
||||
/**
|
||||
* Set a custom notification handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setNotificationHandler(final @NotNull Notification notificationHandler);
|
||||
|
||||
/**
|
||||
* Reloads the entire plugin
|
||||
*/
|
||||
|
||||
@@ -43,8 +43,8 @@ public final class AttackTracker implements JVMProfiler {
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private int peakConnectionsPerSecond;
|
||||
private long peakJoinsPerSecond;
|
||||
private long peakConnectionsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
@@ -52,7 +52,7 @@ public final class AttackTracker implements JVMProfiler {
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
|
||||
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
@@ -69,6 +69,7 @@ public final class AttackTracker implements JVMProfiler {
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().sendAttackNotification();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -79,7 +80,7 @@ public final class AttackTracker implements JVMProfiler {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final int connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
|
||||
// Update connections per second peak if necessary
|
||||
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
|
||||
@@ -112,7 +113,7 @@ public final class AttackTracker implements JVMProfiler {
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
@@ -120,22 +121,23 @@ public final class AttackTracker implements JVMProfiler {
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%peak-cps%", peakCPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
|
||||
.ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%peak-cps%", peakCPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset the attack status
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -36,7 +38,9 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
@@ -47,10 +51,11 @@ public interface SonarCommand {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
|
||||
|
||||
COMMAND_DELAY.cleanUp(); // Clean up the cache
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
@@ -59,7 +64,6 @@ public interface SonarCommand {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
@@ -67,7 +71,7 @@ public interface SonarCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(source);
|
||||
COMMAND_DELAY.put(source.getAudience(), timestamp);
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
enum Language {
|
||||
SYSTEM("system"),
|
||||
AB("Abkhazian"),
|
||||
AA("Afar"),
|
||||
AF("Afrikaans"),
|
||||
SQ("Albanian"),
|
||||
AM("Amharic"),
|
||||
AR("Arabic"),
|
||||
AN("Aragonese"),
|
||||
HY("Armenian"),
|
||||
AS("Assamese"),
|
||||
AE("Avestan"),
|
||||
AY("Aymara"),
|
||||
AZ("Azerbaijani"),
|
||||
BA("Bashkir"),
|
||||
EU("Basque"),
|
||||
BE("Belarusian"),
|
||||
BN("Bengali"),
|
||||
BH("Bihari"),
|
||||
BI("Bislama"),
|
||||
BS("Bosnian"),
|
||||
BR("Breton"),
|
||||
BG("Bulgarian"),
|
||||
MY("Burmese"),
|
||||
CA("Catalan"),
|
||||
CH("Chamorro"),
|
||||
CE("Chechen"),
|
||||
ZH("Chinese"),
|
||||
CU("Slavic"),
|
||||
CV("Chuvash"),
|
||||
KW("Cornish"),
|
||||
CO("Corsican"),
|
||||
HR("Croatian"),
|
||||
CS("Czech"),
|
||||
DA("Danish"),
|
||||
DV("Maldivian"),
|
||||
NL("Dutch"),
|
||||
DZ("Dzongkha"),
|
||||
EN("English"),
|
||||
EO("Esperanto"),
|
||||
ET("Estonian"),
|
||||
FO("Faroese"),
|
||||
FJ("Fijian"),
|
||||
FI("Finnish"),
|
||||
FR("French"),
|
||||
GD("Gaelic"),
|
||||
GL("Galician"),
|
||||
KA("Georgian"),
|
||||
DE("German"),
|
||||
EL("Greek"),
|
||||
GN("Guarani"),
|
||||
GU("Gujarati"),
|
||||
HT("Haitian Creole"),
|
||||
HA("Hausa"),
|
||||
HE("Hebrew"),
|
||||
HZ("Herero"),
|
||||
HI("Hindi"),
|
||||
HO("Hiri Motu"),
|
||||
HU("Hungarian"),
|
||||
IS("Icelandic"),
|
||||
IO("Ido"),
|
||||
ID("Indonesian"),
|
||||
IE("Interlingue"),
|
||||
IU("Inuktitut"),
|
||||
IK("Inupiaq"),
|
||||
GA("Irish"),
|
||||
IT("Italian"),
|
||||
JA("Japanese"),
|
||||
JV("Javanese"),
|
||||
KL("Kalaallisut"),
|
||||
KN("Kannada"),
|
||||
KS("Kashmiri"),
|
||||
KK("Kazakh"),
|
||||
KM("Khmer"),
|
||||
KI("Kikuyu"),
|
||||
RW("Kinyarwanda"),
|
||||
KY("Kirghiz"),
|
||||
KV("Komi"),
|
||||
KO("Korean"),
|
||||
KJ("Kwanyama"),
|
||||
KU("Kurdish"),
|
||||
LO("Lao"),
|
||||
LA("Latin"),
|
||||
LV("Latvian"),
|
||||
LI("Limburgish"),
|
||||
LN("Lingala"),
|
||||
LT("Lithuanian"),
|
||||
LB("Luxembourgish"),
|
||||
MK("Macedonian"),
|
||||
MG("Malagasy"),
|
||||
MS("Malay"),
|
||||
ML("Malayalam"),
|
||||
MT("Maltese"),
|
||||
GV("Manx"),
|
||||
MI("Maori"),
|
||||
MR("Marathi"),
|
||||
MH("Marshallese"),
|
||||
MO("Moldavian"),
|
||||
MN("Mongolian"),
|
||||
NA("Nauru"),
|
||||
NV("Navajo"),
|
||||
ND("Ndebele, North"),
|
||||
NR("Ndebele, South"),
|
||||
NG("Ndonga"),
|
||||
NE("Nepali"),
|
||||
SE("Northern Sami"),
|
||||
NO("Norwegian"),
|
||||
NB("Norwegian Bokmal"),
|
||||
NN("Norwegian Nynorsk"),
|
||||
NY("Chewa"),
|
||||
OC("Occitan"),
|
||||
OR("Oriya"),
|
||||
OM("Oromo"),
|
||||
OS("Ossetian"),
|
||||
PI("Pali"),
|
||||
PA("Panjabi"),
|
||||
FA("Persian"),
|
||||
PL("Polish"),
|
||||
PT("Portuguese"),
|
||||
PS("Pushto"),
|
||||
QU("Quechua"),
|
||||
RM("Raeto-Romance"),
|
||||
RO("Romanian"),
|
||||
RN("Rundi"),
|
||||
RU("Russian"),
|
||||
SM("Samoan"),
|
||||
SG("Sango"),
|
||||
SA("Sanskrit"),
|
||||
SC("Sardinian"),
|
||||
SR("Serbian"),
|
||||
SN("Shona"),
|
||||
II("Sichuan Yi"),
|
||||
SD("Sindhi"),
|
||||
SI("Sinhala"),
|
||||
SK("Slovak"),
|
||||
SL("Slovenian"),
|
||||
SO("Somali"),
|
||||
ST("Sotho, Southern"),
|
||||
ES("Spanish"),
|
||||
SU("Sundanese"),
|
||||
SW("Swahili"),
|
||||
SS("Swati"),
|
||||
SV("Swedish"),
|
||||
TL("Tagalog"),
|
||||
TY("Tahitian"),
|
||||
TG("Tajik"),
|
||||
TA("Tamil"),
|
||||
TT("Tatar"),
|
||||
TE("Telugu"),
|
||||
TH("Thai"),
|
||||
BO("Tibetan"),
|
||||
TI("Tigrinya"),
|
||||
TO("Tonga"),
|
||||
TS("Tsonga"),
|
||||
TN("Tswana"),
|
||||
TR("Turkish"),
|
||||
TK("Turkmen"),
|
||||
TW("Twi"),
|
||||
UG("Uighur"),
|
||||
UK("Ukrainian"),
|
||||
UR("Urdu"),
|
||||
UZ("Uzbek"),
|
||||
VI("Vietnamese"),
|
||||
VO("Volapuk"),
|
||||
WA("Walloon"),
|
||||
CY("Welsh"),
|
||||
FY("Western Frisian"),
|
||||
WO("Wolof"),
|
||||
XH("Xhosa"),
|
||||
YI("Yiddish"),
|
||||
YO("Yoruba"),
|
||||
ZA("Zhuang"),
|
||||
ZU("Zulu");
|
||||
|
||||
private final String name;
|
||||
private final String code;
|
||||
|
||||
Language(final String name) {
|
||||
this.name = name;
|
||||
this.code = name().toLowerCase();
|
||||
}
|
||||
|
||||
public static Language fromCode(final @NotNull String code) {
|
||||
return Language.valueOf(code.toUpperCase());
|
||||
}
|
||||
}
|
||||
@@ -19,64 +19,69 @@ package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
|
||||
import org.simpleyaml.configuration.MemorySection;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
|
||||
|
||||
@Getter
|
||||
public final class SimpleYamlConfig {
|
||||
private final File file;
|
||||
private final YamlFile yaml;
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
String.format("Running Sonar version %s on %s",
|
||||
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
|
||||
"Need help or have questions? https://jonesdev.xyz/discord",
|
||||
"https://github.com/jonesdevelopment/sonar",
|
||||
""
|
||||
);
|
||||
|
||||
public SimpleYamlConfig(final File dataFolder, final String fileName) {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
|
||||
public SimpleYamlConfig(final @NotNull File file) {
|
||||
this.file = file;
|
||||
this.yaml = new YamlFile(file.getPath());
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try {
|
||||
yaml.save();
|
||||
} catch (IOException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
private void replaceConfigFile(final @NotNull URL defaultData) throws Exception {
|
||||
try (final InputStream inputStream = defaultData.openStream()) {
|
||||
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() throws Exception {
|
||||
// https://github.com/jonesdevelopment/sonar/issues/26
|
||||
// Only load the configuration if the file already exists
|
||||
if (yaml.exists()) {
|
||||
yaml.loadWithComments();
|
||||
} else {
|
||||
yaml.createOrLoadWithComments();
|
||||
public void load(final @NotNull URL defaultData) throws Exception {
|
||||
final boolean exists = yaml.exists();
|
||||
|
||||
// Copy the configuration from the translations
|
||||
if (!exists) {
|
||||
replaceConfigFile(defaultData);
|
||||
}
|
||||
|
||||
// Always load the comment format and options
|
||||
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
|
||||
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
|
||||
// Load the configuration
|
||||
yaml.loadWithComments();
|
||||
|
||||
// I hate my life... but this works... somehow
|
||||
if (exists) {
|
||||
// First, we store all values from the current (old) config
|
||||
final Map<String, Object> values = yaml.getValues(true);
|
||||
// Then, we replace the old config file with the default one
|
||||
replaceConfigFile(defaultData);
|
||||
// Re-load the configuration
|
||||
yaml.loadWithComments();
|
||||
// Loop through the new values from the new configuration
|
||||
yaml.getValues(true).forEach((path, v) -> {
|
||||
final Object value = values.get(path);
|
||||
// Make sure we actually need to update the value
|
||||
if (value != null && !value.equals(v)
|
||||
&& !(v instanceof MemorySection)
|
||||
&& !(value instanceof MemorySection)) {
|
||||
// Set the old values to the new values
|
||||
yaml.set(path, value);
|
||||
}
|
||||
});
|
||||
// Save the yaml file to ensure we keep the original values
|
||||
yaml.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void set(final String path, final Object v) {
|
||||
@@ -88,33 +93,81 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getInt(path, def);
|
||||
}
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return 0;
|
||||
}
|
||||
return yaml.getInt(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path, final boolean def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getBoolean(path, def);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return false;
|
||||
}
|
||||
return yaml.getBoolean(path);
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
|
||||
LOGGER.info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = getObject(path);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
throw new IllegalStateException("Invalid entry " + path);
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public Object getObject(final String path) {
|
||||
return yaml.get(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+105
-51
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.controller;
|
||||
|
||||
import com.j256.ormlite.dao.Dao;
|
||||
import com.j256.ormlite.dao.DaoManager;
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.stmt.QueryBuilder;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import com.j256.ormlite.table.TableUtils;
|
||||
@@ -28,12 +29,12 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.dependencies.DependencyLoader;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
@@ -45,6 +46,24 @@ public final class VerifiedPlayerController {
|
||||
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
|
||||
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[database] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[database] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
private @Nullable ConnectionSource connectionSource;
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@@ -57,11 +76,11 @@ public final class VerifiedPlayerController {
|
||||
|
||||
// Don't establish a database connection if the type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
|
||||
LOGGER.warn("Configure a database to save verified players.");
|
||||
return;
|
||||
}
|
||||
|
||||
try (final ConnectionSource connectionSource = DependencyLoader.setUpDriverAndConnect()) {
|
||||
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
|
||||
this.connectionSource = connectionSource;
|
||||
|
||||
// Create table
|
||||
@@ -76,16 +95,89 @@ public final class VerifiedPlayerController {
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
queryBuilder = dao.queryBuilder();
|
||||
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
// Make sure to run the clean task and the caching task in the same thread
|
||||
// https://github.com/jonesdevelopment/sonar/issues/150
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
// Make sure to clear all outdated entries first
|
||||
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
|
||||
// Add all entries from the database to the cache
|
||||
try {
|
||||
dao.queryForAll().forEach(this::_add);
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Could not cache entries: {}", exception);
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
|
||||
LOGGER.error("Error setting up database: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sets up the database connection using the configured credentials
|
||||
*/
|
||||
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
// Prepare the JDBC connection string
|
||||
// TODO: automatically build best connection string for database type
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
|
||||
|
||||
// Use reflection to uncover the driver class
|
||||
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
|
||||
|
||||
// Make sure we send the username and password
|
||||
final Properties credentials = new Properties();
|
||||
credentials.put("user", database.getUsername());
|
||||
credentials.put("password", database.getPassword());
|
||||
|
||||
// Return new JDBC connection instance for our controller to handle
|
||||
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses reflection to load the database driver from the injected library
|
||||
*
|
||||
* @param connectionString URL of the database
|
||||
* @param driverClass Class of the database driver
|
||||
* @param credentials Credentials for the database
|
||||
*/
|
||||
private static Connection invoke(final @NotNull String connectionString,
|
||||
final @NotNull Class<?> driverClass,
|
||||
final @NotNull Properties credentials) throws Throwable {
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
return (Connection) connect.invoke(driver, connectionString, credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
try {
|
||||
final long timestamp = Instant.now()
|
||||
.minus(maximumAge, ChronoUnit.DAYS)
|
||||
.getEpochSecond() * 1000L; // convert to ms
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
LOGGER.info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
LOGGER.error("Error trying to clear old entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First, remove the player from the local cache and then,
|
||||
* secondly, asynchronously add the player to the database.
|
||||
@@ -118,7 +210,7 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
|
||||
LOGGER.error("Error trying to remove entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -156,7 +248,7 @@ public final class VerifiedPlayerController {
|
||||
try {
|
||||
dao.create(player);
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
|
||||
LOGGER.error("Error trying to add entry: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -200,48 +292,10 @@ public final class VerifiedPlayerController {
|
||||
dao.deleteBuilder().delete();
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
|
||||
LOGGER.error("Error trying to clear entries: {}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all old entries using the given timestamp.
|
||||
*/
|
||||
public void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final long timestamp = Instant.now()
|
||||
.minus(maximumAge, ChronoUnit.DAYS)
|
||||
.getEpochSecond() * 1000L; // convert to ms
|
||||
|
||||
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
|
||||
.lt("timestamp", new Timestamp(timestamp))
|
||||
.query();
|
||||
|
||||
if (oldEntries != null) {
|
||||
for (final VerifiedPlayer player : oldEntries) {
|
||||
dao.delete(player);
|
||||
}
|
||||
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
|
||||
oldEntries.size(), maximumAge);
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.dependencies;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
|
||||
public enum Dependency {
|
||||
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.2.0"
|
||||
),
|
||||
// TODO: MariaDB still seems to have some issues; investigate!
|
||||
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
|
||||
MARIADB(
|
||||
"org/mariadb/jdbc",
|
||||
"mariadb-java-client",
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
final @NotNull String version) {
|
||||
final String archiveFileName = String.format("%s-%s.jar", artifactId, version);
|
||||
this.tempFilePath = new File(Sonar.get().getDataDirectory(), "libraries/" + archiveFileName).toPath();
|
||||
|
||||
try {
|
||||
final String mvnCentralPath = String.format("%s/%s/%s/%s-%s.jar",
|
||||
groupId, artifactId, version, artifactId, version);
|
||||
|
||||
this.mvnRepoURL = new URL("https://repo1.maven.org/maven2/" + mvnCentralPath);
|
||||
} catch (MalformedURLException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URL for the local file used for the external URLClassLoader
|
||||
*/
|
||||
public @NotNull URL getClassLoaderURL() throws MalformedURLException {
|
||||
if (!Files.exists(tempFilePath)) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Downloading dependency {}...", tempFilePath.getFileName());
|
||||
|
||||
try (final InputStream inputStream = mvnRepoURL.openStream()) {
|
||||
Files.createDirectories(tempFilePath.getParent());
|
||||
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
|
||||
}
|
||||
|
||||
return tempFilePath.toUri().toURL();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-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.dependencies;
|
||||
|
||||
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
|
||||
import com.j256.ormlite.support.ConnectionSource;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Driver;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.Properties;
|
||||
|
||||
@UtilityClass
|
||||
public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
|
||||
final URL url = database.getType().getDependency().getClassLoaderURL();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
final Connection connection = connect(classLoader, jdbcURL, database);
|
||||
return new JdbcSingleConnectionSource(jdbcURL, connection);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration.Database database) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.put("user", database.getUsername());
|
||||
properties.put("password", database.getPassword());
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
return (Connection) connect.invoke(driver, databaseURL, properties);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-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.dependencies;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
final class ExternalClassLoader extends URLClassLoader {
|
||||
|
||||
/**
|
||||
* Create a URLClassLoader with the system ClassLoader as parent
|
||||
*
|
||||
* @param url URL for of the local file
|
||||
*/
|
||||
public ExternalClassLoader(final URL[] url) {
|
||||
super(url, ClassLoader.getSystemClassLoader().getParent());
|
||||
}
|
||||
|
||||
// Register this URLClassLoader as parallel capable
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
@@ -31,5 +31,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
private final FallbackUser<?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+2
-2
@@ -30,11 +30,11 @@ import java.util.Optional;
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser<?> user;
|
||||
private final FallbackUser user;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser<?> user, final @Nullable String reason) {
|
||||
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final FallbackUser<?> user;
|
||||
private final FallbackUser user;
|
||||
}
|
||||
+1
-1
@@ -33,6 +33,6 @@ import java.util.UUID;
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser<?> user;
|
||||
private final FallbackUser user;
|
||||
private final long timeTakenToVerify;
|
||||
}
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
@@ -31,7 +31,6 @@ import java.net.InetAddress;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -39,10 +38,14 @@ public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
// Map of all connected usernames and their respective IP addresses (used for fast checking)
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@Setter
|
||||
private Cache<InetAddress, Byte> blacklist;
|
||||
@Setter
|
||||
private long blacklistTime;
|
||||
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
|
||||
|
||||
@@ -21,6 +21,5 @@ public interface FallbackPipelines {
|
||||
String FALLBACK_HANDLER = "sonar-fallback-handler";
|
||||
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
|
||||
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
|
||||
String FALLBACK_IN_TRAFFIC = "sonar-traffic-in";
|
||||
String FALLBACK_OUT_TRAFFIC = "sonar-traffic-out";
|
||||
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
|
||||
}
|
||||
@@ -30,26 +30,11 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackQueue {
|
||||
public static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(16, 0.5f);
|
||||
static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
// Runnable task executed for polling the queue
|
||||
private final Runnable pollTask = () -> {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
@@ -71,5 +56,21 @@ public final class FallbackQueue {
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -17,19 +17,19 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackRatelimiter {
|
||||
public static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
@Setter
|
||||
private ExpiringCache<InetAddress> expiringCache;
|
||||
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
private Cache<InetAddress, Byte> expiringCache;
|
||||
|
||||
/**
|
||||
* We don't want to clean up the cache in the
|
||||
@@ -37,7 +37,7 @@ public final class FallbackRatelimiter {
|
||||
* it would take up too many resources.
|
||||
*/
|
||||
public void cleanUpCache() {
|
||||
expiringCache.cleanUp(false);
|
||||
expiringCache.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,9 +49,9 @@ public final class FallbackRatelimiter {
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!expiringCache.has(inetAddress)) {
|
||||
if (!expiringCache.asMap().containsKey(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress);
|
||||
expiringCache.put(inetAddress, (byte) 0);
|
||||
return true;
|
||||
}
|
||||
// Deny the connection attempt
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
@@ -31,32 +31,17 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface FallbackUser<T> {
|
||||
@NotNull Fallback getFallback();
|
||||
|
||||
@NotNull T getConnection();
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull Channel getChannel();
|
||||
|
||||
@NotNull ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
*
|
||||
* @param reason Legacy disconnect message string
|
||||
* @see #disconnect(Component)
|
||||
*/
|
||||
default void disconnect(final @NotNull String reason) {
|
||||
disconnect(Component.text(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
@@ -65,20 +50,78 @@ public interface FallbackUser<T> {
|
||||
*/
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
*
|
||||
* @param username Username of the player
|
||||
* @param uuid UUID of the player
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
* @param boss Name of the main pipeline
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
*
|
||||
* @param msg Message to send to the player
|
||||
*/
|
||||
void write(final @NotNull Object msg);
|
||||
default void write(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().writeAndFlush(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a buffered message that will be
|
||||
* sent once all messages are flushed.
|
||||
*/
|
||||
void delayedWrite(final @NotNull Object msg);
|
||||
default void delayedWrite(final @NotNull Object msg) {
|
||||
if (getChannel().isActive()) {
|
||||
getChannel().write(msg, getChannel().voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
|
||||
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Increments the number of times this user (by IP address) has failed the verification
|
||||
*/
|
||||
default void incrementFails() {
|
||||
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
|
||||
final int failCount = getFailedCount();
|
||||
// Make sure to remove the old values from the cache
|
||||
if (failCount > 0) {
|
||||
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
|
||||
}
|
||||
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of times this user (by IP address) has failed the verification
|
||||
* @apiNote Returns 0 if no key/value is present in the cache
|
||||
*/
|
||||
default int getFailedCount() {
|
||||
// Make sure to clean up the cache before we try to get cached values
|
||||
GLOBAL_FAIL_COUNT.cleanUp();
|
||||
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all previously failed verifications
|
||||
*/
|
||||
default void invalidateFails() {
|
||||
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
@@ -93,10 +136,15 @@ public interface FallbackUser<T> {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
// Only log the failed message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,21 +153,32 @@ public interface FallbackUser<T> {
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
// Use a label, so we can easily add more code beneath this method
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Check if the player has too many failed attempts
|
||||
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
|
||||
final int failCount = getFailedCount() + 1;
|
||||
// Now we simply need to check if the threshold is reached
|
||||
if (failCount >= blacklistThreshold) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
} else {
|
||||
// Cache the InetAddress for 3 minutes
|
||||
PREVIOUS_FAILS.put(getInetAddress().toString());
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
invalidateFails();
|
||||
break blacklist;
|
||||
}
|
||||
|
||||
// Make sure we increment the number of fails
|
||||
incrementFails();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback.traffic;
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
* This counts all incoming and outgoing traffic.
|
||||
*/
|
||||
@Getter
|
||||
public enum TrafficCounter implements JVMProfiler {
|
||||
public enum Bandwidth implements JVMProfiler {
|
||||
INCOMING,
|
||||
OUTGOING;
|
||||
|
||||
@@ -48,7 +48,7 @@ public enum TrafficCounter implements JVMProfiler {
|
||||
* This method is called every second.
|
||||
*/
|
||||
public static synchronized void reset() {
|
||||
for (final TrafficCounter value : values()) {
|
||||
for (final Bandwidth value : values()) {
|
||||
value.cacheAndReset();
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,29 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface Counters {
|
||||
ExpiringCache<Long> LOGINS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
ExpiringCache<Long> CONNECTIONS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
@ApiStatus.Internal
|
||||
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(1))
|
||||
.build();
|
||||
|
||||
@ApiStatus.Internal
|
||||
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(1))
|
||||
.build();
|
||||
|
||||
static long getConnectionsPerSecond() {
|
||||
return CONNECTIONS_PER_SECOND.estimatedSize();
|
||||
}
|
||||
|
||||
static long getLoginsPerSecond() {
|
||||
return LOGINS_PER_SECOND.estimatedSize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
|
||||
public void sendAttackNotification() {
|
||||
// No need to do anything if we have no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Prepare the title
|
||||
// TODO: Why do custom title times not work?
|
||||
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
|
||||
final Title title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
// Prepare the chat message
|
||||
final Component chat = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationChat()
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
|
||||
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
|
||||
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
|
||||
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl()));
|
||||
// Send the title and chat messages to all online players
|
||||
for (final String subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.AUDIENCES.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.showTitle(title);
|
||||
audience.sendMessage(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface Observable {
|
||||
Collection<String> getSubscribers();
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
default boolean isSubscribed(final @NotNull String name) {
|
||||
return getSubscribers().contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
default void subscribe(final String name) {
|
||||
getSubscribers().add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
default void unsubscribe(final String name) {
|
||||
getSubscribers().remove(name);
|
||||
}
|
||||
}
|
||||
@@ -31,27 +31,24 @@ import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements JVMProfiler, Counters {
|
||||
public final class Verbose implements Observable, JVMProfiler, Counters {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Clean up all counters
|
||||
LOGINS_PER_SECOND.cleanUp(false);
|
||||
CONNECTIONS_PER_SECOND.cleanUp(false);
|
||||
LOGINS_PER_SECOND.cleanUp();
|
||||
CONNECTIONS_PER_SECOND.cleanUp();
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
@@ -59,7 +56,7 @@ public final class Verbose implements JVMProfiler, Counters {
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = audiences.get(subscriber);
|
||||
final Audience audience = Sonar.AUDIENCES.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
@@ -75,7 +72,7 @@ public final class Verbose implements JVMProfiler, Counters {
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
|
||||
@@ -100,26 +97,4 @@ public final class Verbose implements JVMProfiler, Counters {
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
name = "Sonar"
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
@@ -31,14 +32,17 @@ dependencies {
|
||||
compileOnly(project(":common"))
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bukkit:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -17,12 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import com.alessiodp.libby.BukkitLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
@@ -34,7 +37,9 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
super(plugin, new BukkitLibraryManager(
|
||||
plugin, plugin.getDataFolder().getName()),
|
||||
plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -78,5 +83,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Bandwidth::reset, 20L, 20L);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
getFallback().getQueue().getPollTask(), 10L, 10L);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Sonar.get().getVerboseHandler()::update, 4L, 4L);
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ public final class AudienceListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
name = "Sonar"
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -19,11 +20,14 @@ dependencies {
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -17,25 +17,29 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import com.alessiodp.libby.BungeeLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -78,12 +82,24 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Bandwidth::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject();
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ public final class AudienceListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.protocol.DefinedPacket;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Channel channel;
|
||||
private @Nullable String username;
|
||||
private ProtocolVersion protocolVersion;
|
||||
private InetAddress inetAddress;
|
||||
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private static final Cache<Component, Kick> CACHED_KICK_PACKETS = Caffeine.newBuilder().build();
|
||||
|
||||
private static final Field CHANNEL_WRAPPER_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
|
||||
CHANNEL_WRAPPER_FIELD.setAccessible(true);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull Kick getCachedKickPacket(final Component component) {
|
||||
Kick packet = CACHED_KICK_PACKETS.asMap().get(component);
|
||||
if (packet == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
final BaseComponent baseComponent = ComponentSerializer.deserialize(serialized);
|
||||
packet = new Kick(baseComponent);
|
||||
CACHED_KICK_PACKETS.put(component, packet);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// The player can disconnect without sending the login packet first
|
||||
if (username != null) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
}
|
||||
// The player cannot be in the queue if the IP address is invalid
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
final DefinedPacket wrappedPacket = packetWrapper.packet;
|
||||
// Don't handle any invalid packets
|
||||
if (wrappedPacket != null) {
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
handleHandshake((Handshake) wrappedPacket);
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (wrappedPacket instanceof LoginRequest) {
|
||||
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
// We can override the default exceptionCaught method since this handler
|
||||
// will run before the MinecraftConnection knows that there has been an error.
|
||||
// Additionally, this will also run after our custom decoder.
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Throwable cause) throws Exception {
|
||||
// Sonar sometimes uses exceptions to quickly close
|
||||
// the channel and interrupt any other ongoing process.
|
||||
// Simply close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getHost().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull LoginRequest loginRequest,
|
||||
final @NotNull Object wrappedMessage) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
// Store the username
|
||||
username = loginRequest.getData();
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(channel, protocolVersion,
|
||||
getCachedKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(channel, protocolVersion,
|
||||
Sonar.get().getConfig().getVerification().getInvalidUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private final ChannelInitializer<Channel> originalChannelInitializer;
|
||||
|
||||
@Override
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
// Invoke the original method
|
||||
try {
|
||||
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
|
||||
}
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.connection.DownstreamBridge;
|
||||
import net.md_5.bungee.connection.UpstreamBridge;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PacketHandler;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
private ChannelWrapper channel;
|
||||
private PacketHandler handler;
|
||||
|
||||
@Override
|
||||
public void setHandler(final @NotNull PacketHandler handler) {
|
||||
this.handler = handler;
|
||||
super.setHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
|
||||
try {
|
||||
handler.exception(throwable);
|
||||
} catch (Exception exception) {
|
||||
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
channel.markClosed();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
-364
@@ -1,364 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.EncryptionUtil;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
// Increase connections per second for the action bar verbose
|
||||
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull Handshake handshake) throws Exception {
|
||||
// Check if the host is invalid
|
||||
if (handshake.getHost().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
super.handle(handshake);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new CorruptedFrameException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// The channel always stays open because the client sends
|
||||
// a StatusRequest and a Ping packet after one another
|
||||
if (!isConnected()) {
|
||||
throw new CorruptedFrameException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new CorruptedFrameException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
final int protocolId = getHandshake().getProtocolVersion();
|
||||
protocolVersion = ProtocolVersion.fromId(protocolId);
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
try {
|
||||
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, this, channel, channel.pipeline(),
|
||||
inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
return;
|
||||
}
|
||||
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
final int limit = bungee.config.getPlayerLimit();
|
||||
if (limit > 0 && bungee.getOnlineCount() >= limit) {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| user.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
if (cachedKickPacket == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (channelWrapper.getHandle().isActive()) {
|
||||
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
channelWrapper.getHandle().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
channelWrapper.getHandle().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@@ -27,20 +27,27 @@ import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@UtilityClass
|
||||
public class BaseInjectionHelper {
|
||||
public void inject(final ChannelInitializer<Channel> childHandler) {
|
||||
public class FallbackInjectionHelper {
|
||||
public void inject() {
|
||||
try {
|
||||
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
|
||||
childField.setAccessible(true);
|
||||
|
||||
// Make sure to store the original channel initializer
|
||||
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
|
||||
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
|
||||
|
||||
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
unsafeField.setAccessible(true);
|
||||
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
|
||||
|
||||
// Get the base object (the object containing the field)
|
||||
final Object base = unsafe.staticFieldBase(childField);
|
||||
// Get the offset of the static field within its class
|
||||
final long offset = unsafe.staticFieldOffset(childField);
|
||||
|
||||
unsafe.putObject(base, offset, childHandler);
|
||||
// Replace the original channel initializer
|
||||
unsafe.putObject(base, offset, fallbackInitializer);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.bungee.fallback.FallbackChannelHandler.getCachedKickPacket;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackLoginListener implements Listener {
|
||||
|
||||
private static final Field CHANNEL_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_FIELD.setAccessible(true);
|
||||
} catch (NoSuchFieldException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
final PendingConnection connection = event.getConnection();
|
||||
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
|
||||
ProtocolVersion.fromId(connection.getVersion()), getCachedKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackUserWrapper implements FallbackUser<FallbackInitialHandler> {
|
||||
private final Fallback fallback;
|
||||
private final FallbackInitialHandler connection;
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.writeAndFlush(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delayedWrite(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.write(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
|
||||
|
||||
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
|
||||
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
|
||||
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
try {
|
||||
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
|
||||
} catch (ChannelException ignored) {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
|
||||
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
|
||||
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
|
||||
|
||||
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
|
||||
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
|
||||
private static final KickStringWriter LEGACY_KICK;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field kickField = PipelineUtils.class.getDeclaredField("legacyKicker");
|
||||
kickField.setAccessible(true);
|
||||
LEGACY_KICK = (KickStringWriter) kickField.get(null);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostly taken from BungeeCord
|
||||
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) throws Exception {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
|
||||
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
|
||||
channel.close();
|
||||
} else {
|
||||
try {
|
||||
BASE.initChannel(channel);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.varint;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
// MessageToMessageDecoder always do release after decoding
|
||||
// https://github.com/jonesdevelopment/sonar-legacy/pull/5
|
||||
return;
|
||||
}
|
||||
|
||||
final VarintByteDecoder reader = new VarintByteDecoder();
|
||||
final int end = byteBuf.forEachByte(reader);
|
||||
|
||||
if (end == -1) {
|
||||
// This is probably a good sign that the buffer was too short or empty
|
||||
// since the ByteBuf cannot hold a proper VarInt.
|
||||
if (reader.getResult() == VarintByteDecoder.DecoderResult.RUN_OF_ZEROES) {
|
||||
byteBuf.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reader.getResult()) {
|
||||
case RUN_OF_ZEROES: {
|
||||
// this will return to the point where the next varInt starts
|
||||
byteBuf.readerIndex(end);
|
||||
break;
|
||||
}
|
||||
|
||||
case SUCCESS: {
|
||||
final int readVarInt = reader.getReadVarInt();
|
||||
|
||||
if (readVarInt < 0) {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
} else if (readVarInt == 0) {
|
||||
// Actually, we don't want to throw an Exception if the packet is empty.
|
||||
// The check would also false flag a lot of legit players since packets
|
||||
// in 1.7 could sometimes be empty.
|
||||
byteBuf.readerIndex(end + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
final int bytesRead = reader.getBytesRead();
|
||||
final int minimumRead = bytesRead + readVarInt;
|
||||
|
||||
if (byteBuf.isReadable(minimumRead)) {
|
||||
out.add(byteBuf.retainedSlice(end + 1, readVarInt));
|
||||
|
||||
byteBuf.skipBytes(minimumRead);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
byteBuf.clear();
|
||||
throw new CorruptedFrameException("Result: " + reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.varint;
|
||||
|
||||
import io.netty.util.ByteProcessor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
|
||||
@Getter
|
||||
@ToString
|
||||
public final class VarintByteDecoder implements ByteProcessor {
|
||||
private DecoderResult result = DecoderResult.TOO_SMALL;
|
||||
private int readVarInt, bytesRead;
|
||||
|
||||
@Override
|
||||
public boolean process(byte b) throws Exception {
|
||||
if (b == 0 && bytesRead == 0) {
|
||||
result = DecoderResult.RUN_OF_ZEROES;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (result == DecoderResult.RUN_OF_ZEROES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
readVarInt |= (b & 0x7F) << bytesRead++ * 7;
|
||||
|
||||
if (bytesRead > 3) {
|
||||
result = DecoderResult.TOO_BIG;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((b & 0x80) != 128) {
|
||||
result = DecoderResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum DecoderResult {
|
||||
SUCCESS,
|
||||
TOO_SMALL,
|
||||
TOO_BIG,
|
||||
RUN_OF_ZEROES
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ dependencies {
|
||||
compileOnly(project(":api"))
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
java.targetCompatibility = JavaVersion.VERSION_1_8
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.Library;
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@UtilityClass
|
||||
class LibraryLoader {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager) {
|
||||
libraryManager.addMavenCentral();
|
||||
libraryManager.addJitPack();
|
||||
libraryManager.loadLibraries(
|
||||
// Simple-YAML
|
||||
Library.builder()
|
||||
.groupId("com{}github{}Carleslc{}Simple-YAML")
|
||||
.artifactId("Simple-Yaml")
|
||||
.version("1.8.4")
|
||||
.relocate("com{}simpleyaml", "xyz{}jonesdev{}sonar{}libs{}yaml")
|
||||
.build(),
|
||||
// Gson
|
||||
Library.builder()
|
||||
.groupId("com{}google{}code{}gson")
|
||||
.artifactId("gson")
|
||||
.version("2.10.1")
|
||||
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
|
||||
.build(),
|
||||
// Caffeine
|
||||
Library.builder()
|
||||
.groupId("com.github.ben-manes.caffeine")
|
||||
.artifactId("caffeine")
|
||||
.version("3.1.8")
|
||||
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
|
||||
.build(),
|
||||
// ORMLite
|
||||
Library.builder()
|
||||
.groupId("com{}j256{}ormlite")
|
||||
.artifactId("ormlite-jdbc")
|
||||
.version("6.1")
|
||||
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
|
||||
.build(),
|
||||
// MySQL driver
|
||||
Library.builder()
|
||||
.groupId("com{}mysql")
|
||||
.artifactId("mysql-connector-j")
|
||||
.version("8.3.0")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,29 +17,27 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.SonarSupplier;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.ServiceRepository;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.Duration;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,24 +45,32 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private T plugin;
|
||||
@Setter
|
||||
private Verbose verboseHandler;
|
||||
@Setter
|
||||
private Notification notificationHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final LibraryManager libraryManager;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final LibraryManager libraryManager,
|
||||
final File dataDirectory,
|
||||
final SonarPlatform platform) {
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
|
||||
// Set the plugin instance before anything else
|
||||
// Store our library manager for the API
|
||||
this.libraryManager = libraryManager;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Set the plugin instance
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.platform = platform;
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
}
|
||||
@@ -86,9 +92,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer);
|
||||
getLogger().info("Enabling all tasks and features...");
|
||||
|
||||
// Start all service threads
|
||||
ServiceRepository.register();
|
||||
|
||||
try {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
@@ -113,19 +116,37 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getConfig().load();
|
||||
|
||||
// Warn player if they reloaded and changed the database type
|
||||
if (getVerifiedPlayerController() != null
|
||||
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
|
||||
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
|
||||
if (verifiedPlayerController != null
|
||||
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
|
||||
getLogger().warn("Reloading the server after changing the database type"
|
||||
+ " is generally not recommended as it can sometimes cause data loss.");
|
||||
}
|
||||
|
||||
// Prepare cached packets
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter
|
||||
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
|
||||
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
|
||||
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
|
||||
// Update ratelimiter cache
|
||||
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
|
||||
final boolean blacklistExists = getFallback().getBlacklist() != null;
|
||||
// Make sure the blacklist is only set when we need it to prevent data loss
|
||||
if (!blacklistExists // Make sure we create a new cache if it doesn't exist yet
|
||||
|| getFallback().getBlacklistTime() != blacklistTime) {
|
||||
// Create new cache with the configured blacklist time
|
||||
getFallback().setBlacklist(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
|
||||
.build());
|
||||
// Store the new blacklist time, so we don't have to reset the blacklist every reload
|
||||
getFallback().setBlacklistTime(blacklistTime);
|
||||
// Warn the user about changing the expiry of the blacklist values
|
||||
if (blacklistExists) {
|
||||
getLogger().warn("The blacklist has been reset as the duration of entries has changed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Reinitialize database controller
|
||||
verifiedPlayerController = new VerifiedPlayerController();
|
||||
@@ -134,8 +155,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
public void shutdown() {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
|
||||
// Shut down service threads
|
||||
ServiceRepository.shutdown();
|
||||
// ...
|
||||
|
||||
getLogger().info("Successfully shut down. Goodbye!");
|
||||
}
|
||||
|
||||
+24
-7
@@ -15,30 +15,47 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.traffic;
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class OutgoingBandwidthEncoder extends ChannelOutboundHandlerAdapter {
|
||||
public static final OutgoingBandwidthEncoder INSTANCE = new OutgoingBandwidthEncoder();
|
||||
public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
public static final ChannelHandler INSTANCE = new FallbackBandwidthHandler();
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the incoming traffic by the amount of readable bytes
|
||||
INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg,
|
||||
final @NotNull ChannelPromise promise) throws Exception {
|
||||
final ChannelPromise promise) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
TrafficCounter.OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
// Increment the outgoing traffic by the amount of readable bytes
|
||||
OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.write(msg, promise);
|
||||
}
|
||||
}
|
||||
-44
@@ -1,44 +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.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
private final @NotNull InetAddress inetAddress;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Make sure other handlers also know we've disconnected
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress"})
|
||||
public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.channel = channel;
|
||||
this.pipeline = channel.pipeline();
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom {@link Disconnect} packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
closeWith(channel, protocolVersion, Disconnect.create(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel Channel to close
|
||||
* @param protocolVersion Protocol version
|
||||
* @param msg Packet to close the channel with
|
||||
*/
|
||||
public static void closeWith(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull Object msg) {
|
||||
if (channel.isActive()) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
channel.config().setAutoRead(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
*
|
||||
* @param username Username of the player
|
||||
* @param uuid UUID of the player
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
*/
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss) {
|
||||
// This rarely happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (pipeline.get(boss) != null) {
|
||||
pipeline.remove(boss);
|
||||
}
|
||||
|
||||
// The player has joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
Sonar.get().getFallback().getConnected().put(username, inetAddress);
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(new LoginSuccess(username, uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
|
||||
}
|
||||
}
|
||||
+105
-73
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
@@ -43,6 +44,7 @@ import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
@@ -50,10 +52,11 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
// General
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser<?> user;
|
||||
private final @NotNull FallbackUser user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
|
||||
@@ -62,7 +65,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private short expectedTransactionId;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets, ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY;
|
||||
private double posX, posY, posZ, lastY, spawnYPosition;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
private boolean listenForMovements;
|
||||
|
||||
@@ -87,7 +90,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser<?> user,
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID playerUuid) {
|
||||
this.user = user;
|
||||
@@ -121,7 +124,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
encoder.updateRegistry(registry);
|
||||
} else {
|
||||
// Something went wrong - the decoder should not be null
|
||||
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
// Close the channel to prevent bypasses or other potential exploits
|
||||
user.getChannel().close();
|
||||
}
|
||||
@@ -172,13 +175,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Set the state to TELEPORT to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TELEPORT;
|
||||
// Make sure the player is unable to fly (the player is in spectator mode)
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Generate the current teleport ID
|
||||
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
|
||||
expectedTeleportId = RANDOM.nextInt();
|
||||
// Add a little randomization to the spawn y coordinate
|
||||
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PositionLook(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, -90f, expectedTeleportId, false));
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
@@ -187,6 +194,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
// If we don't have gravity and captcha enabled, simply finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
&& !FALLBACK.shouldDoMapCaptcha()) {
|
||||
// Save some work by finishing before sending even more packets
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
@@ -212,14 +226,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?> user,
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
|
||||
final String locale) {
|
||||
// Check the client locale by performing a simple regex check on it
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
|
||||
}
|
||||
|
||||
private static boolean validateClientBrand(final @NotNull FallbackUser<?> user, final ByteBuf content) {
|
||||
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
|
||||
// We have to catch every DecoderException, so we can fail and punish
|
||||
// the player instead of only disconnecting them due to an exception.
|
||||
try {
|
||||
@@ -227,11 +241,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// already performs these checks by default.
|
||||
final String read = ProtocolUtil.readBrandMessage(content);
|
||||
// Check if the decoded client brand string is too long
|
||||
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
|
||||
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
|
||||
return false;
|
||||
}
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
return !read.equals("Vanilla") // The normal brand is always lowercase
|
||||
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
|
||||
} catch (DecoderException exception) {
|
||||
@@ -242,57 +256,60 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePacketDuringCaptcha(final @NotNull FallbackPacket packet) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof Chat) {
|
||||
final Chat chat = (Chat) packet;
|
||||
Objects.requireNonNull(captcha);
|
||||
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(1000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
|
||||
user.write(new Chat(MiniMessage.miniMessage().deserialize(
|
||||
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
|
||||
// Make sure to reset the timer
|
||||
actionBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Every 10 seconds
|
||||
if (keepAlive.elapsed(10_000L)) {
|
||||
// Send the message again to remind the player
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
|
||||
// Send both packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
|
||||
// We are expecting a captcha code in chat, drop all other packets
|
||||
if (state == State.MAP_CAPTCHA) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof Chat) {
|
||||
final Chat chat = (Chat) packet;
|
||||
Objects.requireNonNull(captcha);
|
||||
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(1000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
|
||||
user.write(new Chat(MiniMessage.miniMessage().deserialize(
|
||||
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
|
||||
// Make sure to reset the timer
|
||||
actionBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Every about 10 seconds
|
||||
if (keepAlive.elapsed(timeout)) {
|
||||
// Send the message again to remind the player
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
|
||||
// Send both packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
handlePacketDuringCaptcha(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,11 +317,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check if the time limit has exceeded
|
||||
if (login.elapsed(timeout)) {
|
||||
user.getChannel().close();
|
||||
return;
|
||||
}
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
checkFrame(!login.elapsed(timeout), "time limit exceeded");
|
||||
|
||||
if (packet instanceof LoginAcknowledged) {
|
||||
// Check if we are currently expecting a LoginAcknowledged packet
|
||||
@@ -374,7 +389,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
}
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.PLUGIN_MESSAGE) {
|
||||
@@ -454,7 +471,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
|
||||
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
|
||||
// Then, check if the position is equal to the spawn position
|
||||
&& x == SPAWN_X_POSITION && y == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
@@ -478,7 +495,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
listenForMovements = true;
|
||||
}
|
||||
|
||||
lastY = dynamicSpawnYPosition;
|
||||
lastY = spawnYPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,7 +510,10 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
@@ -502,15 +522,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (tick > maxMovementTick) {
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// The player is colliding to blocks, finish verification
|
||||
if (ground) {
|
||||
// If the collision check is disabled, finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions()) {
|
||||
captchaOrFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the collision check
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player is colliding to blocks, finish verification
|
||||
captchaOrFinish();
|
||||
return;
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
@@ -525,7 +557,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
}
|
||||
|
||||
@@ -537,7 +569,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void captchaOrFinish() {
|
||||
if (user.getFallback().shouldDoMapCaptcha()) {
|
||||
if (FALLBACK.shouldDoMapCaptcha()) {
|
||||
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
|
||||
// This should not happen, but we have to return if there is no captcha prepared
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
@@ -581,7 +613,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void finish() {
|
||||
state = State.SUCCESS;
|
||||
|
||||
user.getFallback().getConnected().remove(username);
|
||||
FALLBACK.getConnected().remove(username);
|
||||
|
||||
// Add verified player to the database
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
|
||||
@@ -594,7 +626,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
|
||||
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.toString()));
|
||||
}
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackUser<?> user;
|
||||
private final FallbackUser user;
|
||||
private final FallbackPacketListener listener;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser<?> user,
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser user,
|
||||
final @NotNull FallbackPacketListener listener) {
|
||||
this.user = user;
|
||||
this.listener = listener;
|
||||
|
||||
+2
-1
@@ -69,6 +69,7 @@ public class FallbackPreparer {
|
||||
|
||||
// Blocks
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
public BlockType blockType = BlockType.BARRIER;
|
||||
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
@@ -109,7 +110,7 @@ public class FallbackPreparer {
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, blockType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -17,6 +17,7 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
@@ -26,7 +27,7 @@ import java.util.function.Function;
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum BlockType {
|
||||
STONE(protocolVersion -> 1),
|
||||
STONE(protocolVersion -> 1, 1D),
|
||||
BARRIER(protocolVersion -> {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
@@ -79,11 +80,14 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20_3:
|
||||
return 10366;
|
||||
}
|
||||
});
|
||||
}, 1D);
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
private final Function<ProtocolVersion, Integer> idFunction;
|
||||
// TODO: Implement per-version block heights?
|
||||
@Getter
|
||||
private final double blockHeight;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
return idFunction.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -61,7 +61,7 @@ public final class DimensionRegistry {
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public final class PreparedMapInfo {
|
||||
this.modern = new MapData(info);
|
||||
}
|
||||
|
||||
public void write(final @NotNull FallbackUser<?> user) {
|
||||
public void write(final @NotNull FallbackUser user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
for (final MapData data : legacy) {
|
||||
user.delayedWrite(data);
|
||||
|
||||
+3
-4
@@ -37,11 +37,10 @@ public final class Position implements FallbackPacket {
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
// https://github.com/jonesdevelopment/sonar/issues/20
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.skipBytes(8);
|
||||
}
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
+3
-4
@@ -42,11 +42,10 @@ public final class PositionLook implements FallbackPacket {
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
// https://github.com/jonesdevelopment/sonar/issues/20
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.skipBytes(8);
|
||||
}
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
pitch = byteBuf.readFloat();
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.traffic;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class IncomingBandwidthDecoder extends ChannelInboundHandlerAdapter {
|
||||
public static final IncomingBandwidthDecoder INSTANCE = new IncomingBandwidthDecoder();
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
TrafficCounter.INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +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.traffic;
|
||||
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
|
||||
|
||||
@UtilityClass
|
||||
public class TrafficChannelHooker implements FallbackPipelines {
|
||||
public void hook(final @NotNull ChannelPipeline pipeline,
|
||||
final @NotNull String decoder,
|
||||
final @NotNull String encoder) {
|
||||
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
|
||||
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
|
||||
}
|
||||
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
|
||||
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@UtilityClass
|
||||
public class ServiceRepository {
|
||||
private final ScheduledExecutorService QUEUE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService TRAFFIC_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService VERBOSE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private boolean registered;
|
||||
|
||||
public void register() {
|
||||
if (registered) {
|
||||
throw new IllegalStateException("Already registered");
|
||||
}
|
||||
registered = true;
|
||||
|
||||
TRAFFIC_SERVICE.scheduleAtFixedRate(TrafficCounter::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
QUEUE_SERVICE.scheduleAtFixedRate(Sonar.get().getFallback().getQueue()::poll,
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
VERBOSE_SERVICE.scheduleAtFixedRate(Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
if (!registered) {
|
||||
throw new IllegalStateException("Not registered");
|
||||
}
|
||||
registered = false;
|
||||
|
||||
QUEUE_SERVICE.shutdown();
|
||||
TRAFFIC_SERVICE.shutdown();
|
||||
VERBOSE_SERVICE.shutdown();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -28,7 +28,8 @@ public final class SubcommandRegistryHolder implements SubcommandRegistry {
|
||||
new StatisticsCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand()
|
||||
new DumpCommand(),
|
||||
new NotifyCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -54,7 +54,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
if (SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress);
|
||||
SONAR.getFallback().getBlacklist().put(inetAddress, (byte) 0);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
@@ -85,20 +85,20 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
if (!SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress);
|
||||
SONAR.getFallback().getBlacklist().invalidate(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
|
||||
final long blacklisted = SONAR.getFallback().getBlacklist().estimatedSize();
|
||||
|
||||
if (blacklisted == 0) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
|
||||
@@ -106,7 +106,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
SONAR.getFallback().getBlacklisted().invalidateAll();
|
||||
SONAR.getFallback().getBlacklist().invalidateAll();
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
|
||||
break;
|
||||
@@ -114,7 +114,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
case "size": {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize())));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "notify",
|
||||
description = "Enable or disable attack notifications",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class NotifyCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getName())) {
|
||||
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsUnsubscribed());
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsSubscribed());
|
||||
SONAR.getNotificationHandler().subscribe(invocation.getSender().getName());
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -23,8 +23,8 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
@@ -74,7 +74,7 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
|
||||
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%uptime%", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L))
|
||||
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
@@ -105,10 +105,10 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
|
||||
|
||||
case NETWORK: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
|
||||
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
|
||||
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
|
||||
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
|
||||
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
|
||||
.replace("%incoming%", Bandwidth.INCOMING.getCachedSecond())
|
||||
.replace("%outgoing%", Bandwidth.OUTGOING.getCachedSecond())
|
||||
.replace("%ttl_incoming%", Bandwidth.INCOMING.getCachedTtl())
|
||||
.replace("%ttl_outgoing%", Bandwidth.OUTGOING.getCachedTtl()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -22,26 +22,23 @@ import io.netty.util.AttributeKey;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
/**
|
||||
* Simple utility to determine if someone joins using GeyserMC
|
||||
*/
|
||||
@UtilityClass
|
||||
public class GeyserUtil {
|
||||
|
||||
/*
|
||||
* Geyser attribute key for every floodgate player
|
||||
*/
|
||||
private AttributeKey<Object> playerAttribute() {
|
||||
return AttributeKey.valueOf("floodgate-player");
|
||||
}
|
||||
// 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");
|
||||
|
||||
/**
|
||||
* @param channel Channel of the player
|
||||
* @return Whether the player is on GeyserMC or not
|
||||
*/
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel) {
|
||||
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
|
||||
// Every floodgate player has a channel attribute called "floodgate-player"
|
||||
return channel.attr(playerAttribute()).get() != null;
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel,
|
||||
final @NotNull InetSocketAddress originalAddress) {
|
||||
return originalAddress.getPort() == 0 // check for floodgate on the server
|
||||
|| channel.attr(PLAYER_ATTRIBUTE).get() != null; // check for standalone Geyser proxy connection
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
|
||||
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development a Sonar Contributors, licencováno pod GPL-3.0
|
||||
#
|
||||
|
||||
# Obecné nastavení pro Sonar
|
||||
general:
|
||||
# Měl by Sonar vypisovat IP adresy hráču do konzole?
|
||||
log-player-addresses: true
|
||||
# Maximum hráču online se stejnou IP adresou
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Nastavení citlivosti pro sledovač útoku
|
||||
# Změnte jenom pokud víte co děláte!
|
||||
attack-tracker:
|
||||
# Minimum nových hráču aby sledovač zaregistroval útok
|
||||
min-players-for-attack: 8
|
||||
# Čas, který musí uběhnout aby byl útok zaregistrovan jako ukončený
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
min-attack-duration: 30000
|
||||
# Počet, kolikrát musí být incident reportován, aby byl označen jako útok
|
||||
# Toto číslo figuruje jako buffer (tlumič) na filtrovaní chybných notifikací
|
||||
min-attack-threshold: 2
|
||||
# Čas, který musí uběhnout aby byl další útok detekován
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# Můžete se připojit Sonar k databázi aby uložil ověřené hráče ponechal i po restartu serveru
|
||||
# Pozn.: IP adresy nejsou jakkoliv šifrovány, naopak jsou ponechány jako čistý text! Je vaše zodpovědnost ponechat tuto databázi bezpečnou!
|
||||
# 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 (Žádný, MYSQL)
|
||||
type: NONE
|
||||
# Host adresa pro verifikování hráčů s SQL databází
|
||||
host: localhost
|
||||
# Port pro verifikaci s databází
|
||||
port: 3306
|
||||
# Jméno SQL databáze
|
||||
name: sonar
|
||||
# Jméno pro verifikaci přes SQL databázi
|
||||
username: ''
|
||||
# Heslo pro verifikaci přes SQL databázi
|
||||
password: ''
|
||||
# Kolik dní by měl Sonar ponechat hráče v databázi?
|
||||
maximum-age: 5
|
||||
|
||||
# Každý nový login bude přidán do řady kvuli prevenci spam útoků
|
||||
# Řada je aktulizována každých 500 milisekund (10 ticků)
|
||||
queue:
|
||||
# Maximum součastných aktualizací řady za 500 milisekund
|
||||
max-polls: 30
|
||||
|
||||
# Každý připojený hráč bude přepojen do
|
||||
# Jednoduchého limbo serveru kde proběhnou zkoušky proti botům
|
||||
verification:
|
||||
# Kdy by měl Sonar verifikovat nové hráče?
|
||||
# TYPY: ALWAYS, DURING_ATTACK, NEVER (Vždy, Pouze při útoku, Nikdy)
|
||||
# - ALWAYS: Noví hráči budou vždy verifikováni (Doporučeno)
|
||||
# - DURING_ATTACK: New players will only be checked during an attack Noví hráči budou verifikováni pouze při útoku
|
||||
# - NEVER: Noví hráči nebudou nikdy vyžkoušeni
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Kontroluje, zda pohyb hráčů je roven gravitační formuli Minecraftu
|
||||
# Všechny předpokládané pohyby jsou předpočítány, kvůli ušetření výkonu
|
||||
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
|
||||
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
|
||||
max-movement-ticks: 8
|
||||
# Maximum ignorovaných změn Y-nové souřadnice hráče, aby neprošel verifikací
|
||||
max-ignored-ticks: 5
|
||||
# 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
|
||||
|
||||
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
|
||||
map-captcha:
|
||||
# Kdy by měl Sonar nechat hráče verifikovat pomocí mapy?
|
||||
# Typy: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Noví hráči budou vždy verifikováni pomocí CAPTCHY?
|
||||
# - DURING_ATTACK: Noví hráči dostanou map captchu jenom při útoku
|
||||
# - NEVER: Noví hráči nikdy nedostanou map CAPTCHU (Doporučeno)
|
||||
timing: NEVER
|
||||
# Měl by Sonar randomizovat pozici X a Y na CAPTCHE?
|
||||
random-position: true
|
||||
# Jak moc by měl Sonar znehodnotit charaktery (faktor pro randomizaci)
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# Kolik náhodných čar by měl Sonar vykreslit za CAPTCHOU
|
||||
random-lines: 4
|
||||
# Kolik náhodných oválů by měl Sonar vykreslit za CAPTCHOU
|
||||
random-ovals: 1
|
||||
# Měl by Sonar randomizovat font pro vykreslování textu v CAPTCHE?
|
||||
random-font-size: true
|
||||
# Kolik odpovědí by měl Sonar předgenerovat?
|
||||
# Tento úkol běží souběžně v pozadí;
|
||||
# Hráči jsou schopni se připojit pokud je CAPTCHA připravena
|
||||
precompute: 1000
|
||||
# Jak dlouho by měl Sonar čekat než hráč selže CAPTCHU?
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
max-duration: 45000
|
||||
# Kolikrát musí hráč selhat CAPTCHU před celkovém selhání verifikace?
|
||||
max-tries: 3
|
||||
# Znaky (čísla a písmena), které jsou povoleny k zobrazování v CAPTCHE
|
||||
dictionary: '123456789'
|
||||
# Jaké typy fontů by měl Sonar používat pro map CAPTCHU?
|
||||
fonts:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
|
||||
# Zkouší pokuď hráč posílá správný "client brand packet" serveru
|
||||
client-brand:
|
||||
# Měl by Sonar zkoušet pro validitu "client brandu"? (Doporučeno)
|
||||
enabled: true
|
||||
# Regex pro validitaci "client brand"
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# Maximální délka "client brand" při verifikaci
|
||||
max-length: 64
|
||||
|
||||
# Regex for validování jmen při verifikaci
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex for validaci "client locale" při verifikaci
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maximální odezva hráče aby byl odpojen
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
max-ping: 10000
|
||||
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
|
||||
max-login-packets: 256
|
||||
|
||||
# Měl by Sonar posílat nové verifikace?
|
||||
log-connections: true
|
||||
# Měl by Sonar posílat nové verifikace při útoku?
|
||||
log-during-attack: false
|
||||
# Měl by Sonar posílat každý jeden pohyb při verifikaci?
|
||||
# Toto nastavení není doporučeno pro servry v produkci, ale může být nápomocné při vyhledávaní chyb
|
||||
debug-xyz-positions: false
|
||||
# Množství času který musí uběhnout aby hráč byl odpojen
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
read-timeout: 3500
|
||||
# How long should a player wait before reconnecting during verification Jak dlouho by měl hráč
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
rejoin-delay: 8000
|
||||
# Jak dlouho by měla být IP adresa zakázána k připojení po selhání verifikace mockrát?
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
blacklist-time: 600000
|
||||
# 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)
|
||||
# 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: []
|
||||
@@ -0,0 +1,172 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Need help or have questions? https://jonesdev.xyz/discord
|
||||
# Source code: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# General settings for Sonar
|
||||
general:
|
||||
# Should Sonar log players' IP addresses in console?
|
||||
log-player-addresses: true
|
||||
# Maximum number of players online with the same IP address
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Sensitivity settings for the attack tracker
|
||||
# Only edit this if you know what you're doing
|
||||
attack-tracker:
|
||||
# Minimum number of new players in order for an attack to be detected
|
||||
min-players-for-attack: 8
|
||||
# Amount of time that has to pass in order for an attack to be over
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
min-attack-duration: 30000
|
||||
# Number of times an incident has to be reported in order to be acknowledged as an attack
|
||||
# This number acts as a buffer to filter out false attack notifications
|
||||
min-attack-threshold: 2
|
||||
# Amount of time that has to pass in order for a new attack to be detected
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# You can connect Sonar to a database to keep verified players even after restarting your server
|
||||
# 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
|
||||
type: NONE
|
||||
# Host address for authenticating with the SQL database
|
||||
host: localhost
|
||||
# Port for authenticating with the SQL database
|
||||
port: 3306
|
||||
# Name of the SQL database
|
||||
name: sonar
|
||||
# Username for authenticating with the SQL database
|
||||
username: ''
|
||||
# Password for authenticating with the SQL database
|
||||
password: ''
|
||||
# How many days should Sonar keep verified players in the database?
|
||||
maximum-age: 5
|
||||
|
||||
# Every new login request will be queued to avoid spam join attacks
|
||||
# The queue is updated every 500 milliseconds (10 ticks)
|
||||
queue:
|
||||
# Maximum number of concurrent queue polls per 500 milliseconds
|
||||
max-polls: 30
|
||||
|
||||
# Every new player that joins for the first time will be sent to
|
||||
# a lightweight limbo server where advanced bot checks are performed
|
||||
verification:
|
||||
# When should Sonar verify new players?
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be checked (Recommended)
|
||||
# - DURING_ATTACK: New players will only be checked during an attack
|
||||
# - NEVER: New players will never be checked
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Checks if the players' falling motion is following Minecraft's gravity formula
|
||||
# All predicted motions are precalculated in order to save performance
|
||||
gravity:
|
||||
# Should Sonar check for valid client gravity? (Recommended)
|
||||
enabled: true
|
||||
# Should Sonar check if the player collides with blocks? (Recommended)
|
||||
check-collisions: true
|
||||
# Maximum number of ticks the player has to fall in order to be allowed to hit the platform
|
||||
max-movement-ticks: 8
|
||||
# Maximum number of ignored Y movement changes before a player fails verification
|
||||
max-ignored-ticks: 5
|
||||
# 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
|
||||
|
||||
# Make the player type a code from a virtual map in chat
|
||||
map-captcha:
|
||||
# When should Sonar make the player solve a CAPTCHA?
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always receive a CAPTCHA
|
||||
# - DURING_ATTACK: New players will only receive a CAPTCHA during an attack
|
||||
# - NEVER: New players will never receive a CAPTCHA (Recommended)
|
||||
timing: NEVER
|
||||
# Should Sonar randomize the X and Y position of the CAPTCHA?
|
||||
random-position: true
|
||||
# How much should Sonar distort characters (factor for randomization)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# How many random lines behind the CAPTCHA should Sonar draw?
|
||||
random-lines: 4
|
||||
# How many random ovals behind the CAPTCHA should Sonar draw?
|
||||
random-ovals: 1
|
||||
# Should Sonar randomize the size of the font used for rendering the CAPTCHA?
|
||||
random-font-size: true
|
||||
# How many answers should Sonar prepare?
|
||||
# This task happens asynchronously in the background;
|
||||
# Players are able to join once one CAPTCHA has been prepared
|
||||
precompute: 1000
|
||||
# How long should Sonar wait until the player fails the CAPTCHA?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
max-duration: 45000
|
||||
# How many times must a player fail the CAPTCHA before failing the verification?
|
||||
max-tries: 3
|
||||
# Characters (letters and numbers) that are allowed to appear in the answer to the CAPTCHA
|
||||
dictionary: '123456789'
|
||||
# Which font types should Sonar use for the map CAPTCHA codes?
|
||||
fonts:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
|
||||
# Checks if the players is sending a valid client brand to the server
|
||||
client-brand:
|
||||
# Should Sonar check for valid client brands? (Recommended)
|
||||
enabled: true
|
||||
# Regex for validating client brands during verification
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# Maximum client brand length during verification
|
||||
max-length: 64
|
||||
|
||||
# Regex for validating usernames during verification
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex for validating client locale during verification
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Ping a player has to have in order to timeout
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
max-ping: 10000
|
||||
# Maximum number of login packets the player has to send in order to be kicked
|
||||
max-login-packets: 256
|
||||
|
||||
# Should Sonar log new verification attempts?
|
||||
log-connections: true
|
||||
# Should Sonar log new verification attempts during attacks?
|
||||
log-during-attack: false
|
||||
# Should Sonar log every single movement/position change during verification?
|
||||
# This is not recommended for production servers but can be helpful for spotting errors.
|
||||
debug-xyz-positions: false
|
||||
# Amount of time that has to pass before a player times out
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
read-timeout: 3500
|
||||
# How long should a player wait before reconnecting during verification
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
rejoin-delay: 8000
|
||||
# How long should an IP address be denied from logging in when failing the verification too often?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
blacklist-time: 600000
|
||||
# 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)
|
||||
# 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: []
|
||||
@@ -0,0 +1,172 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# დახმარება გჭირდება ან კითხვა გაქვს? https://jonesdev.xyz/discord
|
||||
# საწყისი კოდი: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# ზოგადი პარამეტრები Sonar-ისთვის
|
||||
general:
|
||||
# უნდა ჩაწეროს თუ არა Sonar-მა მოთამაშეების IP მისამართები კონსოლში?
|
||||
log-player-addresses: true
|
||||
# მაქსიმალური ერთი და იგივე IP ზე ონლაინ მოთამაშეთა რაოდენობა
|
||||
max-online-per-ip: 3
|
||||
|
||||
# მნიშვნელოვანი პარამეტრები თავდასხმის დაფიქსირებისთვის
|
||||
# შეცვალეთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
|
||||
attack-tracker:
|
||||
# ახალი მოთამაშეების მინიმალური რაოდენობა, რათა დაფიქსირდეს შეტევა
|
||||
min-players-for-attack: 8
|
||||
# დრო, რის შემდეგაც შეტევა დასრულებული ჩაითვლება
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
min-attack-duration: 30000
|
||||
# რამდენჯერ უნდა მოხდეს ზედა პარამეტრები, რათა ჩაითვალოს ეს თავდასხმად
|
||||
# ეს რიცხვი მოქმედებს ყალბი თავდასხმის შეტყობინებების გასაფილტრად
|
||||
min-attack-threshold: 2
|
||||
# დრო, რომელიც უნდა გავიდეს ახალი შეტევის დასადგენად
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# თქვენ შეგიძლიათ დააკავშიროთ Sonar მონაცემთა ბაზას, რომ შეინახოთ ვერიფირებული მოთამაშეები თქვენი სერვერის გადატვირთვის შემდეგაც კი
|
||||
# შენიშვნა: IP მისამართები ინახება უბრალო ტექსტში. თქვენ ხართ პასუხისმგებელი თქვენი მონაცემთა ბაზის უსაფრთხოებაზე!
|
||||
database:
|
||||
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს ვერიფირებული მოთამაშეების შესანახად
|
||||
# შესაძლო ტიპები: NONE, MYSQL
|
||||
type: NONE
|
||||
# ჰოსტის მისამართი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
host: localhost
|
||||
# პორტი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
port: 3306
|
||||
# SQL მონაცემთა ბაზის სახელი
|
||||
name: sonar
|
||||
# მომხმარებლის სახელი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
username: ''
|
||||
# პაროლი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
|
||||
password: ''
|
||||
# რამდენი დღე უნდა შეინახოს Sonar-მა დამოწმებული მოთამაშეები მონაცემთა ბაზაში?
|
||||
maximum-age: 5
|
||||
|
||||
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ სპამის მიერთების შეტევები
|
||||
# რიგი განახლდება ყოველ 500 მილიწამში (10 ტიკი)
|
||||
queue:
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 500 მილიწამში
|
||||
max-polls: 30
|
||||
|
||||
# ყოველი ახალი მოთამაშე, რომელიც პირველად შეუერთდება, გაიგზავნება
|
||||
# ლიმბო სერვერში, სადაც მოთამაშეების ვერიფიკაცია ხდება
|
||||
verification:
|
||||
# როდის უნდა გადაამოწმოს Sonar-მა ახალი მოთამაშეები?
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოწმდება (რეკომენდებულია)
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროს
|
||||
# - NEVER: ახალი მოთამაშეები არასოდეს შემოწმდებიან
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# ამოწმებს, მიჰყვება თუ არა მოთამაშეების დაცემის მოძრაობა Minecraft-ის გრავიტაციის ფორმულას
|
||||
# ყველა პროგნოზირებული მოძრაობა წინასწარ არის გათვლილი პერფორმანსის შესანარჩუნებლად
|
||||
gravity:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა კლიენტის გრავიტაცია? (რეკომენდირებულია)
|
||||
enabled: true
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
|
||||
check-collisions: true
|
||||
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
|
||||
max-movement-ticks: 8
|
||||
# იგნორირებული Y მოძრაობის მაქსიმალური რაოდენობა სანამ, მოთამაშე ვერიფიკაციას ვერ გაივლის
|
||||
max-ignored-ticks: 5
|
||||
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
|
||||
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
|
||||
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
|
||||
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
|
||||
map-captcha:
|
||||
# როდის უნდა გააკეთებინოს Sonar-მა მოთამაშეს CAPTCHA?
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოუწევთ CAPTCHA-ს გაკეთება
|
||||
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
|
||||
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
|
||||
timing: NEVER
|
||||
# მოახდინოს თუ არა Sonar-მა CAPTCHA-ს X და Y პოზიციების რანდომიზაცია?
|
||||
random-position: true
|
||||
# რამდენად უნდა დაამახინჯოს Sonar-მა სიმბოლოები (რანდომიზაციის ფაქტორი)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# რამდენი რანდომული ხაზი უნდა დახატოს Sonar-მა CAPTCHA-ს უკან?
|
||||
random-lines: 4
|
||||
# რამდენი რანდომული ოვალი უნდა დახატოს სონარმა CAPTCHA-ს უკან?
|
||||
random-ovals: 1
|
||||
# დაარანდომაიზოს Sonar-მა თუ არა შერჩევისას შრიფტის ზომა, რომელიც გამოიყენება CAPTCHA-ზე?
|
||||
random-font-size: true
|
||||
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
|
||||
# ეს ხდება ასინქრონულად უკანა ფონზე;
|
||||
# მოთამაშეებს შეუძლიათ შესვლა, როდესაც ერთი CAPTCHA მზად არის
|
||||
precompute: 1000
|
||||
# რამდენ ხანს უნდა დაელოდოს Sonar-ი, სანამ მოთამაშე ვერ გააკეთებს წარმატებულად CAPTCHA-ს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
max-duration: 45000
|
||||
# რამდენჯერ ვერ უდნა გაიაროს CAPTCHA მოთამაშემ სანამ ვერიფიკაციას ვერ გაივლის?
|
||||
max-tries: 3
|
||||
# სიმბოლოები (ასოები და რიცხვები), რომლებიც დაშვებულია გამოჩნდეს CAPTCHA-ს პასუხში
|
||||
dictionary: '123456789'
|
||||
# რომელი შრიფტის ტიპები უნდა გამოიყენოს Sonar-მა რუკის CAPTCHA კოდებისთვის?
|
||||
fonts:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
|
||||
#ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
|
||||
client-brand:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა ნამდვილია თუ არა მოთამაშის კლიენტის სახელი? (რეკომენდებულია)
|
||||
enabled: true
|
||||
# Regex-ი კლიენტის ბრენდების დასადასტურებლად ვერიფიკაციის დროს
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# მოთამაშის კლიენტის სახელის მაქსიმალური სიგრძე ვერიფიკაციის დროს
|
||||
max-length: 64
|
||||
|
||||
# Regex-ი მომხმარებლის სახელების ვერიფიკაციისთვის
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex-ი კლიენტის ლოკალის დასადასტურებლად ვერიფიკაციის დროს
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# პინგი რამდენიც უნდა ქონდეს მოთამაშეს იმისთვის, რომ ტაიმაუტდეს
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
max-ping: 10000
|
||||
# მაქსიმალური რაოდენობა ლოგინ პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გაიკიკება სერვერიდან
|
||||
max-login-packets: 256
|
||||
|
||||
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
|
||||
log-connections: true
|
||||
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
|
||||
log-during-attack: false
|
||||
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
|
||||
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
|
||||
debug-xyz-positions: false
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
read-timeout: 3500
|
||||
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
rejoin-delay: 8000
|
||||
# რამდენ ხნით უნდა აეკრძალოს IP მისამართს შესვლა, როდესაც ვერიფიკაციას ძალიან ხშირად ვერ აკეთებს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
blacklist-time: 600000
|
||||
# რამდენად ხშირად უნდა გაიაროს მოთამაშემ ვერიფიკაცია წარუმატებლად, რომ ის მოხვდეს შავ სიაში?
|
||||
# (დააყენეთ ეს 0 ზე, რომ მთლიანად გათიშოთ შავი სია)
|
||||
blacklist-threshold: 2
|
||||
# პროტოკოლის ID-ების სია, რომლებიც არ არის შემოწმებული Sonar-ის მიერ (დამოწმების შემოვლითი გზა)
|
||||
# თქვენ შეგიძლიათ იპოვოთ ყველა პროტოკოლის ID-ების სრული სია აქ:
|
||||
# https://wiki.vg/Protocol_version_numbers
|
||||
# მაგალითად, Minecraft 1.20-ს აქვს ID 763.
|
||||
whitelisted-protocols: []
|
||||
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
|
||||
blacklisted-protocols: []
|
||||
@@ -0,0 +1,172 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
|
||||
# Исходный код: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# Основные настройки
|
||||
general:
|
||||
# Должен ли Sonar логировать IP-адреса игроков в консоли?
|
||||
log-player-addresses: true
|
||||
# Максимальное количество игроков онлайн с одного IP-адреса
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Настройки чувствительности для трекера атак
|
||||
# Редактируйте это только в том случае, если вы знаете, что делаете.
|
||||
attack-tracker:
|
||||
# Минимальное количество новых игроков, чтобы атака была обнаружена
|
||||
min-players-for-attack: 8
|
||||
# Количество времени, которое должно пройти, чтобы атака закончилась
|
||||
# (Это значение представляет собой время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
min-attack-duration: 30000
|
||||
# Количество раз, которое необходимо сообщить об инциденте, чтобы он был признан атакой.
|
||||
# Это число служит буфером для отсеивания ложных уведомлений об атаках
|
||||
min-attack-threshold: 2
|
||||
# Количество времени, которое должно пройти, чтобы новая атака была обнаружена.
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# Вы можете подключить Sonar к базе данных, чтобы сохранять проверенных игроков даже после перезапуска сервера.
|
||||
# Примечание: IP-адреса сохраняются в виде обычного текста. Вы сами отвечаете за сохранность своей базы данных!
|
||||
database:
|
||||
# Тип базы данных, которую Sonar использует для хранения проверенных игроков.
|
||||
# Возможные типы: NONE, MYSQL
|
||||
type: NONE
|
||||
# Адрес хоста для аутентификации в базе данных SQL
|
||||
host: localhost
|
||||
# Порт для аутентификации с базой данных SQL
|
||||
port: 3306
|
||||
# Имя базы данных SQL
|
||||
name: sonar
|
||||
# Имя пользователя для аутентификации в базе данных SQL
|
||||
username: ''
|
||||
# Пароль для аутентификации в базе данных SQL
|
||||
password: ''
|
||||
# Сколько дней Sonar должен хранить проверенных игроков в базе данных?
|
||||
maximum-age: 5
|
||||
|
||||
# Каждый новый запрос на вход на сервер будет ставиться в очередь, чтобы избежать спам-атак.
|
||||
# Очередь обновляется каждые 500 миллисекунд (10 тиков)
|
||||
queue:
|
||||
# Максимальное количество одновременных опросов очереди за 500 миллисекунд
|
||||
max-polls: 30
|
||||
|
||||
# Каждый новый игрок, впервые присоединившийся на сервер, будет отправлен на
|
||||
# лимбо-сервер, на котором проводятся расширенные проверки ботов
|
||||
verification:
|
||||
# Когда Sonar должен проверять новых игроков?
|
||||
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Новые игроки будут проверяться всегда (Рекомендуется)
|
||||
# - DURING_ATTACK: Новые игроки будут проверяться только во время атаки
|
||||
# - NEVER: Новые игроки никогда не будут проверены
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Проверяет, следует ли движение игрока формуле гравитации Minecraft.
|
||||
# Все предсказанные движения предварительно просчитываются для экономии производительности
|
||||
gravity:
|
||||
# Должен ли Sonar проверять наличие действительной гравитации клиента? (Рекомендуется)
|
||||
enabled: true
|
||||
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
|
||||
check-collisions: true
|
||||
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
|
||||
max-movement-ticks: 8
|
||||
# Максимальное количество проигнорированных изменений движения Y, прежде чем игрок не пройдет проверку
|
||||
max-ignored-ticks: 5
|
||||
# Режим игрока во время проверки
|
||||
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: все компоненты пользовательского интерфейса видны
|
||||
# - CREATIVE: здоровье и голод скрыты
|
||||
# - ADVENTURE: все компоненты пользовательского интерфейса видны
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# Заставить игрока ввести код с картинки в чат
|
||||
map-captcha:
|
||||
# Когда Sonar должен заставить игрока решить CAPTCHA?
|
||||
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Новые игроки всегда получают капчу
|
||||
# - DURING_ATTACK: Новые игроки будут получать капчу только во время атаки
|
||||
# - NEVER: Новые игроки никогда не получат капчу (Рекомендуется)
|
||||
timing: NEVER
|
||||
# Должен ли Sonar рандомизировать положение X и Y капчи?
|
||||
random-position: true
|
||||
# Насколько сильно Sonar должен искажать символы (фактор для рандомизации)?
|
||||
distortions-factor-x: 50
|
||||
distortions-factor-y: 50
|
||||
# Сколько случайных линий за капчей должен нарисовать Sonar?
|
||||
random-lines: 4
|
||||
# Сколько случайных овалов за капчей должен нарисовать Sonar?
|
||||
random-ovals: 1
|
||||
# Должен ли Sonar рандомизировать размер шрифта, используемого для отображения капчи?
|
||||
random-font-size: true
|
||||
# Сколько капч должен подготовить Sonar?
|
||||
# Эта задача выполняется асинхронно в фоновом режиме
|
||||
# Игроки смогут присоединиться, как только будет подготовлена одна капча
|
||||
precompute: 1000
|
||||
# Как долго Sonar должен ждать, пока игрок не пройдет капча?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
max-duration: 45000
|
||||
# Сколько раз игрок должен завалить капчу, чтобы не пройти проверку?
|
||||
max-tries: 3
|
||||
# Символы (буквы и цифры), которые разрешено использовать в ответе на капчу
|
||||
dictionary: '123456789'
|
||||
# Какие типы шрифтов следует использовать в Sonar для текста в капче?
|
||||
fonts:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- Serif
|
||||
- SansSerif
|
||||
|
||||
# Проверяет, отправляет ли игрок на сервер действительный клиентский бренд.
|
||||
client-brand:
|
||||
# Должен ли Sonar проверять наличие действительных клиентских брендов? (Рекомендуется)
|
||||
enabled: true
|
||||
# Regex для проверки брендов клиентов во время верификации
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# Максимальная длина бренда клиента при проверке
|
||||
max-length: 64
|
||||
|
||||
# Regex для проверки ников при верификации
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex для проверки локализации клиента при верификации
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Пинг который должен быть у игрока чтобы не пройти проверку
|
||||
# (Это значение представляет собой время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
max-ping: 10000
|
||||
# Максимальное количество пакетов для входа на сервер, которое игрок должен отправить, чтобы быть выгнанным
|
||||
max-login-packets: 256
|
||||
|
||||
# Должен ли Sonar регистрировать новые попытки проверки?
|
||||
log-connections: true
|
||||
# Должен ли Sonar регистрировать новые попытки проверки во время атак?
|
||||
log-during-attack: false
|
||||
# Должен ли Sonar регистрировать каждое движение/изменение положения во время проверки?
|
||||
# Это не рекомендуется для рабочих серверов, но может быть полезно для выявления ошибок.
|
||||
debug-xyz-positions: false
|
||||
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
read-timeout: 3500
|
||||
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
rejoin-delay: 8000
|
||||
# Как долго IP-адресу должно быть запрещено входить на сервер, если он слишком часто не проходит проверку?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
blacklist-time: 600000
|
||||
# Как часто игрок должен проваливать проверку, чтобы попасть в черный список?
|
||||
# (Установите это значение на 0, чтобы полностью отключить черный список)
|
||||
blacklist-threshold: 2
|
||||
# Список протоколов, которые не проверяются Sonar (обход проверки)
|
||||
# Полный список всех идентификаторов протоколов можно найти здесь:
|
||||
# https://wiki.vg/Protocol_version_numbers
|
||||
# Например, Minecraft 1.20 имеет ID 763.
|
||||
whitelisted-protocols: []
|
||||
# Список протоколов, которые вообще не могут присоединиться к серверу
|
||||
blacklisted-protocols: []
|
||||
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Need help or have questions? https://jonesdev.xyz/discord
|
||||
# Source code: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# Which language should Sonar use for all configurations?
|
||||
# For this to take effect, restart your server or reload Sonar.
|
||||
#
|
||||
# Values will not be translated if you change your language
|
||||
# when all configuration files already exist.
|
||||
# If you still want all configuration files to be translated,
|
||||
# you have to reset the configuration, messages, and webhook settings.
|
||||
#
|
||||
# You can view a full list of valid language codes here:
|
||||
# https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||
# If a translation does not exist yet, Sonar will use English (en)
|
||||
#
|
||||
# If you set this option to "system", Sonar will use the system language.
|
||||
# Please keep this file unchanged if you don't know what you're doing.
|
||||
language=system
|
||||
@@ -0,0 +1,330 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Need help or have questions? 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: https://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>If you believe that this is an error, contact an administrator.
|
||||
# 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>There are too many players online with your IP address.
|
||||
- '%footer%'
|
||||
|
||||
# Translations for commands
|
||||
commands:
|
||||
# Message that is shown when a player tries running /sonar without permission
|
||||
no-permission: '%prefix%<red>You do not have permission to execute this command.'
|
||||
# Message that is shown when someone uses a command incorrectly
|
||||
incorrect-usage: '%prefix%<red>Usage: /sonar %usage%'
|
||||
# Message that is shown when someone provides an invalid IP address (Invalid format)
|
||||
invalid-ip-address: '%prefix%The IP address you provided seems to be invalid.'
|
||||
# Message that is shown when someone provides an illegal IP address (Local IP)
|
||||
illegal-ip-address: '%prefix%The IP address you provided seems to be either a local
|
||||
or loopback IP.'
|
||||
# Message that is shown when the console runs a command that is player-only
|
||||
player-only: '%prefix%<red>You can only execute this command as a player.'
|
||||
# Message that is shown when a player runs a command that is console-only
|
||||
console-only: '%prefix%<red>For security reasons, you can only execute this command
|
||||
through console.'
|
||||
# Message that is shown when a player executes Sonar commands too quickly
|
||||
cool-down: '%prefix%<red>You can only execute this command every 0.5 seconds.'
|
||||
cool-down-left: '%prefix%<red>Please wait another <bold>%time-left%s<reset><red>.'
|
||||
# Message that is shown when a player does not have permission to execute a certain subcommand
|
||||
subcommand-no-permission: '%prefix%<red>You do not have permission to execute this
|
||||
subcommand. <gray>(%permission%)'
|
||||
|
||||
# Translations for '/sonar'
|
||||
main:
|
||||
# Informational message that is shown above everything when running the main command
|
||||
header:
|
||||
- <yellow>Running <b>Sonar</b> %version% on %platform%.
|
||||
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
|
||||
Jones Development and Sonar Contributors</click>
|
||||
- ''
|
||||
- <gray>Need help or have any questions?
|
||||
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click
|
||||
to open Discord)'>Open a ticket on the Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click
|
||||
to open GitHub)'>or open a new issue on GitHub.
|
||||
- ''
|
||||
# Formatting of the list of subcommands shown when running the main command
|
||||
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Only
|
||||
players: </gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission:
|
||||
</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%''><dark_aqua>
|
||||
▪ <gray>/sonar %subcommand% <white>%description%'
|
||||
|
||||
# Translations for '/sonar reload'
|
||||
reload:
|
||||
# Message that is shown when someone starts reloading Sonar
|
||||
start: '%prefix%Reloading Sonar...'
|
||||
# Message that is shown when Sonar has finished reloading
|
||||
finish: '%prefix%<green>Successfully reloaded <gray>(%taken%ms)'
|
||||
|
||||
# Translations for '/sonar verbose'
|
||||
verbose:
|
||||
# Message that is shown when a player subscribes to Sonar verbose
|
||||
subscribed: '%prefix%You are now viewing Sonar verbose.'
|
||||
# Message that is shown when a player unsubscribes from Sonar verbose
|
||||
unsubscribed: '%prefix%You are no longer viewing Sonar verbose.'
|
||||
|
||||
# Translations for '/sonar notify'
|
||||
notify:
|
||||
# Message that is shown when a player subscribes to Sonar attack notifications
|
||||
subscribed: '%prefix%You are now viewing Sonar attack notifications.'
|
||||
# Message that is shown when a player unsubscribes from Sonar attack notifications
|
||||
unsubscribed: '%prefix%You are no longer viewing Sonar attack notifications.'
|
||||
|
||||
# Translations for '/sonar blacklist'
|
||||
blacklist:
|
||||
# Message that is shown when someone tries clearing the blacklist but is is empty
|
||||
empty: '%prefix%The blacklist is currently empty. Therefore, no IP addresses were
|
||||
removed from the blacklist.'
|
||||
# Message that is shown when someone clears the blacklist
|
||||
cleared: '%prefix%You successfully removed a total of %removed% IP address(es)
|
||||
from the blacklist.'
|
||||
# Message that is shown when someone checks the size of the blacklist
|
||||
size: '%prefix%The blacklist currently contains %amount% IP address(es).'
|
||||
# Message that is shown when someone adds an IP address to the blacklist
|
||||
added: '%prefix%Successfully added %ip% to the blacklist.'
|
||||
# Message that is shown when someone adds an IP address to the blacklist that is verified
|
||||
added-warning: '%prefix%<red>Warning: <white>%ip% is currently whitelisted. Consider
|
||||
removing the IP address from the list of verified players to avoid potential
|
||||
issues.'
|
||||
# Message that is shown when someone removes an IP address from the blacklist
|
||||
removed: '%prefix%Successfully removed %ip% from the blacklist.'
|
||||
# Message that is shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
duplicate-ip: '%prefix%The IP address you provided is already blacklisted.'
|
||||
# Message that is shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
ip-not-found: '%prefix%The IP address you provided is not blacklisted.'
|
||||
|
||||
# Translations for '/sonar verified'
|
||||
verified:
|
||||
# Message that is shown when someone tries clearing the list of verified players but is empty
|
||||
empty: '%prefix%The list of verified players is currently empty. Therefore, no
|
||||
players were unverified.'
|
||||
# Message that is shown when someone clears the list of verified players
|
||||
cleared: '%prefix%You successfully unverified a total of %removed% unique player(s).'
|
||||
# Message that is shown when someone checks the size of the list of verified players
|
||||
size: '%prefix%There are currently %amount% unique player(s) verified.'
|
||||
# Message that is shown when someone un-verifies an IP address
|
||||
removed: '%prefix%Successfully unverified %ip%.'
|
||||
# Message that is shown when someone un-verifies an IP address, but it is not verified
|
||||
ip-not-found: '%prefix%The IP address you provided is not verified.'
|
||||
# Message that is shown when someone tries un-verifying the same IP address twice (double operation)
|
||||
blocked: '%prefix%Please wait for the current operation to finish.'
|
||||
|
||||
# Translations for '/sonar statistics'
|
||||
statistics:
|
||||
# Informational message that is shown above everything when viewing the statistics
|
||||
header: '%prefix%Showing %type% statistics for this session:'
|
||||
# Message that is shown when a player tries viewing an unknown statistic
|
||||
unknown-type: '%prefix%<red>Unknown statistics type! Available statistics: <gray>%statistics%'
|
||||
# Format of the general statistics message
|
||||
general:
|
||||
- ' <dark_aqua>▪ <gray>Verified IP addresses: <white>%verified%'
|
||||
- ' <dark_aqua>▪ <gray>Verifying IP addresses: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Blacklisted IP addresses: <white>%blacklisted%'
|
||||
- ' <dark_aqua>▪ <gray>Currently queued logins: <white>%queued%'
|
||||
- ' <dark_aqua>▪ <gray>Total non-unique joins: <white>%total_joins%'
|
||||
- ' <dark_aqua>▪ <gray>Total verification attempts: <white>%total_attempts%'
|
||||
- ' <dark_aqua>▪ <gray>Total failed verifications: <white>%total_failed%'
|
||||
- ' <dark_aqua>▪ <gray>Server uptime: <white>%uptime%'
|
||||
# Format of the CPU statistics message
|
||||
cpu:
|
||||
- ' <dark_aqua>▪ <gray>Process CPU usage right now: <white>%process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>System CPU usage right now: <white>%system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Per-core process CPU usage: <white>%average_process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Per-core system CPU usage: <white>%average_system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>General system load average: <white>%load_average%%'
|
||||
- ' <dark_aqua>▪ <gray>Total amount of virtual cpus: <white>%virtual_cores%'
|
||||
# Format of the memory statistics message
|
||||
memory:
|
||||
- ' <dark_aqua>▪ <gray>Total free memory: <white>%free_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Total used memory: <white>%used_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Total maximum memory: <white>%max_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Total allocated memory: <white>%total_memory%'
|
||||
# Format of the network statistics message
|
||||
network:
|
||||
- ' <dark_aqua>▪ <gray>Current incoming used bandwidth: <white>%incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Current outgoing used bandwidth: <white>%outgoing%'
|
||||
- ' <dark_aqua>▪ <gray>Total incoming used bandwidth: <white>%ttl_incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Total outgoing used bandwidth: <white>%ttl_outgoing%'
|
||||
|
||||
# Translations for all messages during the verification process
|
||||
verification:
|
||||
# Translations for all debug messages during the verification
|
||||
logs:
|
||||
# Message that is logged to console whenever a new player joins the server
|
||||
connection: '%name%%ip% (%protocol%) has connected.'
|
||||
# Message that is logged to console whenever a player fails verification
|
||||
failed: '%ip% (%protocol%) has failed the bot check for: %reason%'
|
||||
# Message that is logged to console whenever a player is blacklisted
|
||||
blacklisted: '%ip% (%protocol%) was blacklisted for too many failed attempts'
|
||||
# Message that is logged to console whenever a player is verified
|
||||
successful: '%name% has been verified successfully (%time%s!).'
|
||||
|
||||
# Message that is shown to the player when they are being checked for valid gravity
|
||||
welcome: '%prefix%<gray>Please wait a moment for the verification to finish...'
|
||||
|
||||
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
|
||||
captcha:
|
||||
# Message that is shown to the player when they have to enter the answer to the captcha
|
||||
enter-code: '%prefix%<green>Please enter the code in chat that is displayed on
|
||||
the map.'
|
||||
# Timer that is shown to the player when they have to enter the answer to the captcha
|
||||
# (Set this to '' to disable the action bar message)
|
||||
action-bar: '%prefix%<green>You have %time-left% seconds left to enter the code
|
||||
in chat'
|
||||
# Message that is shown to the player when they enter the wrong answer in chat
|
||||
incorrect: '%prefix%<red>You have entered the wrong code. Please try again.'
|
||||
|
||||
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>Your anti-bot data has not been prepared yet.
|
||||
- <gray>Please wait a few seconds before trying to verify again.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when too many players are verifying at the same time
|
||||
too-many-players:
|
||||
- '%header%'
|
||||
- <gold>Too many players are currently trying to log in, try again later.
|
||||
- <gray>Please wait a few seconds before trying to join again.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone rejoins too fast during verification
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>You reconnected too fast, try again later.
|
||||
- <gray>Please wait a few seconds before trying to verify again.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone joins but is already verifying
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>Your IP address is currently being verified.
|
||||
- <red>Please wait a few seconds before trying to verify again.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone joins but is already queued for verification
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>Your IP address is currently queued for verification.
|
||||
- <red>Please wait a few minutes before trying to verify again.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone joins but is temporarily blacklisted
|
||||
blacklisted:
|
||||
- '%header%'
|
||||
- <red>You are currently denied from entering the server.
|
||||
- <red>Please wait a few minutes to be able to join the server again.
|
||||
- <gold>False positive? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone joins with an invalid username
|
||||
invalid-username:
|
||||
- '%header%'
|
||||
- <red>Your username contains invalid characters.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone joins with a blacklisted version
|
||||
blacklisted-protocol:
|
||||
- '%header%'
|
||||
- <red>You are using a version that is not allowed on our server.
|
||||
- <gold>Need help logging in? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone tries verifying with an account that is online
|
||||
already-online:
|
||||
- '%header%'
|
||||
- <red>There is someone already online with your account.
|
||||
- <gray>Please wait a few seconds before trying to verify again.
|
||||
- <gray>If this keeps occurring, try restarting your game or contact support.
|
||||
- '%footer%'
|
||||
# Disconnect message that is shown when someone verifies successfully
|
||||
success:
|
||||
- '%header%'
|
||||
- <green>You have successfully passed the verification.
|
||||
- <white>You are now able to play on the server when you reconnect.
|
||||
# Disconnect message that is shown when someone fails verification
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>You have failed the verification.
|
||||
- <gray>Please wait a few seconds before trying to verify again.
|
||||
- <gold>Need help? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
# Translations for Sonar's actionbar verbose messages
|
||||
verbose:
|
||||
layout:
|
||||
# Layout for Sonar's actionbar verbose
|
||||
# Placeholders:
|
||||
# - '%queued%' Number of queued connections
|
||||
# - '%verifying%' Number of verifying connections
|
||||
# - '%blacklisted%' Number of blacklisted IP addresses
|
||||
# - '%total-joins%' Number of total attempted joins
|
||||
# - '%verify-total%' Number of total verification attempts
|
||||
# - '%verify-success%' Number of verified IP addresses
|
||||
# - '%verify-failed%' Number of failed verifications
|
||||
# - '%logins-per-second%' Number of logins per second
|
||||
# - '%connections-per-second%' Number of connections per second
|
||||
# - '%attack-duration%' Duration of the current attack
|
||||
# - '%incoming-traffic%' Incoming bandwidth usage per second
|
||||
# - '%outgoing-traffic%' Outgoing bandwidth usage per second
|
||||
# - '%incoming-traffic-ttl%' Total incoming bandwidth usage
|
||||
# - '%outgoing-traffic-ttl%' Total outgoing bandwidth usage
|
||||
# - '%used-memory%' Amount of used memory of the process
|
||||
# - '%total-memory%' Amount of total memory of the process
|
||||
# - '%max-memory%' Amount of max memory of the process
|
||||
# - '%free-memory%' Amount of free memory of the process
|
||||
# - '%animation%' Customizable animated symbol
|
||||
# Translations for Sonar's normal verbose output
|
||||
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%<reset>'
|
||||
# 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%<reset>'
|
||||
# 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>A bot attack has been detected
|
||||
# Layout for Sonar's attack notification in chat
|
||||
chat:
|
||||
- ''
|
||||
- <yellow><b>Sonar<reset>
|
||||
- ''
|
||||
- <gray>A bot attack has been detected.
|
||||
- ' <dark_aqua>▪ <gray>Logins per second: <white>%logins-per-second%'
|
||||
- ' <dark_aqua>▪ <gray>Verifying players: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Queued players: <white>%queued%'
|
||||
- ''
|
||||
- <gray>View more information using <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:'(Click to run)'>/sonar stats</hover></click><gray>.
|
||||
- ''
|
||||
@@ -0,0 +1,333 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Нужна помощь или есть вопросы? 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-usage: '%prefix%<red>Применение: /sonar %usage%'
|
||||
# Сообщение, которое отображается, когда кто-то предоставляет неверный формат IP-адреса
|
||||
invalid-ip-address: '%prefix%Указанный вами IP-адрес недействителен'
|
||||
# Сообщение, которое отображается, когда кто-то предоставляет локальный IP-адрес
|
||||
illegal-ip-address: '%prefix%IP-адрес который вы указали является локальным.'
|
||||
# Сообщение, которое отображается, когда консоль выполняет команду, предназначенную только для игроков
|
||||
player-only: '%prefix%<red>Эта команда может быть выполнена только игроком.'
|
||||
# Сообщение, которое отображается, когда игрок выполняет команду, доступную только для консоли
|
||||
console-only: '%prefix%<red>Эту команду можно выполнить только только через консоль.'
|
||||
# Сообщение, которое появляется, когда игрок выполняет команды плагина слишком быстро
|
||||
cool-down: '%prefix%<red>Эту команду можно использовать только каждые 0,5 секунды.'
|
||||
cool-down-left: '%prefix%<red>Подождите еще <bold>%time-left%s<reset><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: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Только
|
||||
игроки: </gray>%only_players%<br><gray>Требуется консоль: </gray>%require_console%<br><gray>Permission:
|
||||
</gray><white>%permission%<br><gray>Псевдонимы: </gray>%aliases%''><dark_aqua>
|
||||
▪ <gray>/sonar %subcommand% <white>%description%'
|
||||
|
||||
# Перевод для '/sonar reload'
|
||||
reload:
|
||||
# Сообщение, которое отображается, когда кто-то начинает перезагружать Sonar
|
||||
start: '%prefix%Перезагрузка...'
|
||||
# Сообщение, которое отображается, когда Sonar завершает перезагрузку
|
||||
finish: '%prefix%<green>Успешная перезагрузка <gray>(%taken%ms)'
|
||||
|
||||
# Перевод для '/sonar verbose'
|
||||
verbose:
|
||||
# Сообщение, которое показывается, когда игрок подписывается на Sonar verbose
|
||||
subscribed: '%prefix%Вы просматриваете подробную информацию о Sonar.'
|
||||
# Сообщение, которое показывается, когда игрок отписывается от Sonar verbose
|
||||
unsubscribed: '%prefix%Вы больше не просматриваете подробную информацию о Sonar.'
|
||||
|
||||
# Перевод для '/sonar notify'
|
||||
notify:
|
||||
# Сообщение, которое отображается, когда игрок подписывается на уведомления об атаках
|
||||
subscribed: '%prefix%Вы получаете уведомления об атаках.'
|
||||
# Сообщение, которое отображается, когда игрок отписывается на уведомления об атаках
|
||||
unsubscribed: '%prefix%Вы больше не получаете уведомления об атаках.'
|
||||
|
||||
# Перевод для '/sonar blacklist'
|
||||
blacklist:
|
||||
# Сообщение, которое отображается, когда кто-то пытается очистить черный список, но он пустой
|
||||
empty: '%prefix%В настоящее время черный список пуст. Поэтому ни один IP-адрес
|
||||
не был удален из черного списка.'
|
||||
# Сообщение, которое показывается, когда кто-то очищает черный список
|
||||
cleared: '%prefix%Вы успешно удалили %удалено% IP-адресов из черного списка.'
|
||||
# Сообщение, которое показывается, когда кто-то проверяет размер черного списка
|
||||
size: '%prefix%В настоящее время черный список содержит %amount% IP-адрес(ов).'
|
||||
# Message that is shown when someone adds an IP address to the blacklist
|
||||
added: '%prefix%Успешно добавлен %ip% в черный список.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
|
||||
added-warning: '%prefix%<red>Предупреждение: <white>%ip% находится в белом списке.
|
||||
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы
|
||||
избежать потенциальных ошибок.'
|
||||
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
|
||||
removed: '%prefix%Успешно удален %ip% из черного списка.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
|
||||
duplicate-ip: '%prefix%Указанный вами IP-адрес уже внесен в черный список.'
|
||||
# Сообщение, которое отображается, когда кто-то удаляет IP-адрес из черного списка, но он не занесен в черный список
|
||||
ip-not-found: '%prefix%Указанный вами IP-адрес не внесен в черный список.'
|
||||
|
||||
# Перевод для '/sonar verified'
|
||||
verified:
|
||||
# Сообщение, которое отображается, когда кто-то пытается очистить список проверенных игроков, но он пустой
|
||||
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни один игрок не был удален.'
|
||||
# Сообщение, которое отображается, когда кто-то очищает список проверенных игроков
|
||||
cleared: '%prefix%Вы успешно очистили %removed% уникального игрока(ов).'
|
||||
# Сообщение, которое показывается, когда кто-то проверяет размер списка проверенных игроков
|
||||
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
|
||||
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
|
||||
removed: '%prefix%Успешно удален %ip%.'
|
||||
# Сообщение, которое отображается, когда кто-то удаляет IP-адрес из списка, но его нет в списке
|
||||
ip-not-found: '%prefix%Указанный вами IP-адрес не в списке.'
|
||||
# Сообщение, которое отображается, когда кто-то пытается дважды удалить один и тот же IP-адрес (двойная операция)
|
||||
blocked: '%prefix%Пожалуйста, дождитесь завершения текущей операции.'
|
||||
|
||||
# Перевод для '/sonar statistics'
|
||||
statistics:
|
||||
# Информационное сообщение, которое отображается поверх всего при просмотре статистики
|
||||
header: '%prefix%Показываем статистику %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>Всего неуникальных соединений: <white>%total_joins%'
|
||||
- ' <dark_aqua>▪ <gray>Общее количество попыток проверки: <white>%total_attempts%'
|
||||
- ' <dark_aqua>▪ <gray>Всего неудачных попыток провери: <white>%total_failed%'
|
||||
- ' <dark_aqua>▪ <gray>Время работы сервера: <white>%uptime%'
|
||||
# Формат сообщения статистики процессора
|
||||
cpu:
|
||||
- ' <dark_aqua>▪ <gray>Использование процессора сервером в данный момент: <white>%process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Использование процессора системой прямо сейчас: <white>%system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Использование процессора каждым ядром (сервером): <white>%average_process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Использование процессора каждым ядром (системой): <white>%average_system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Средняя нагрузка на систему: <white>%load_average%%'
|
||||
- ' <dark_aqua>▪ <gray>Общее количество виртуальных ядер: <white>%virtual_cores%'
|
||||
# Формат сообщения статистики памяти
|
||||
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%'
|
||||
- ' <dark_aqua>▪ <gray>Текущая исходящая используемая полоса пропускания: <white>%outgoing%'
|
||||
- ' <dark_aqua>▪ <gray>Общая входящая использованная полоса пропускания: <white>%ttl_incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Общая исходящая использованная полоса пропускания: <white>%ttl_outgoing%'
|
||||
|
||||
# Переводы всех сообщений в процессе проверки
|
||||
verification:
|
||||
# TПереводы всех отладочных сообщений во время проверки
|
||||
logs:
|
||||
# Сообщение, которое выводится в консоль каждый раз, когда к серверу присоединяется новый игрок
|
||||
connection: '%name%%ip% (%protocol%) подключился.'
|
||||
# Сообщение, которое выводится в консоль при неудачной проверке игрока
|
||||
failed: '%ip% (%protocol%) не прошел проверку бота на: %reason%'
|
||||
# Сообщение, которое выводится в консоль, когда игрок попадает в черный список
|
||||
blacklisted: '%ip% (%protocol%) был занесен в черный список за слишком большое
|
||||
количество неудачных попыток'
|
||||
# Сообщение, которое выводится в консоль при успешной проверке игрока
|
||||
successful: '%name% прошел проверку (%time%s!).'
|
||||
|
||||
# Сообщение, которое показывается игроку при проверке на наличие ванильной гравитации
|
||||
welcome: '%prefix%<gray>Пожалуйста, подождите некоторое время для завершения проверки...'
|
||||
|
||||
captcha:
|
||||
# Сообщение, которое показывается игроку, когда он должен ввести ответ на капчу
|
||||
enter-code: '%prefix%<green>Введите код в чате, который отображается на карте.'
|
||||
# Таймер, который показывается игроку, когда он должен ввести ответ на капчу
|
||||
# (Установите это значение на '' что-бы отключить эту функцию)
|
||||
action-bar: '%prefix%<green>У вас осталось %time-left% секунд, чтобы ввести код
|
||||
в чате'
|
||||
# Сообщение, которое показывается игроку, когда он вводит неправильный код капчи в чате
|
||||
incorrect: '%prefix%<red>Вы ввели неправильный код. Пожалуйста, попробуйте еще
|
||||
раз.'
|
||||
|
||||
# Сообщение об отключении, которое отображается, когда кто-то присоединяется но капча еще не подготовлена
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>Капча еще не подготовлена.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается при одновременной верификации слишком большого количества игроков
|
||||
too-many-players:
|
||||
- '%header%'
|
||||
- <gold>Слишком много игроков пытаются войти на сервер.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается, когда кто-то слишком быстро перезаходит во время проверки
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>Вы перезаходите слишком быстро, повторите попытку позже.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается, когда кто-то присоединяется, но его IP-адрес уже проходит верификацию
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>Ваш IP-адрес в настоящее время проверяется.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то присоединяется, но уже находится в очереди на проверку
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>Ваш IP-адрес в настоящее время находится в очереди на проверку.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то присоединяется, но временно находится в черном списке
|
||||
blacklisted:
|
||||
- '%header%'
|
||||
- <red>Вам запрещено входить на сервер.
|
||||
- <red>Пожалуйста, подождите несколько минут, чтобы снова присоединиться к серверу.
|
||||
- <gold>Вы считаете что это ошибка? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то присоединяется с неправильным ником
|
||||
invalid-username:
|
||||
- '%header%'
|
||||
- <red>Ваш ник содержит недопустимые символы.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается, когда кто-то присоединяется с неподдерживаемой версией
|
||||
invalid-protocol:
|
||||
- '%header%'
|
||||
- <red>Ваша версия в настоящее время не поддерживается.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то заходит на сервер версией, внесенной в черный список
|
||||
blacklisted-protocol:
|
||||
- '%header%'
|
||||
- <red>Вы используете версию, которая запрещена на нашем сервере.
|
||||
- <gold>Нужна помощь? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то пытается пройти верификацию с помощью ника, который находится онлайн.
|
||||
already-online:
|
||||
- '%header%'
|
||||
- <red>Пользователь с таким ником уже онлайн.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- <gray>Если это продолжается, попробуйте перезапустить игру или обратитесь к
|
||||
администратору.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается при успешной верификации
|
||||
success:
|
||||
- '%header%'
|
||||
- <green>Вы успешно прошли проверку.
|
||||
- <white>Пожалуйста, перезайдите.
|
||||
# Сообщение об отключении, которое показывается, когда кто-то не прошел верификацию
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>Вы не прошли проверку.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- <gold>Нужна помощь? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
# Перевод для action bar (verbose)
|
||||
verbose:
|
||||
layout:
|
||||
# Макет для панели действий Sonar verbose
|
||||
# Placeholders:
|
||||
# - '%queued%' Количество игроков в очереди
|
||||
# - '%verifying%' Количество проверяемых игроков
|
||||
# - '%blacklisted%' Количество IP-адресов в черном списке
|
||||
# - '%total-joins%' Количество всех попыток соединения
|
||||
# - '%verify-total%' Количество попыток верификации
|
||||
# - '%verify-success%' Количество проверенных IP-адресов
|
||||
# - '%verify-failed%' Количество неудачных проверок
|
||||
# - '%logins-per-second%' Количество входов на сервер в секунду
|
||||
# - '%connections-per-second%' Количество соединений в секунду
|
||||
# - '%attack-duration%' Продолжительность текущей атаки
|
||||
# - '%incoming-traffic%' Использование входящего трафика в секунду
|
||||
# - '%outgoing-traffic%' Использование исходящего трафика в секунду
|
||||
# - '%incoming-traffic-ttl%' Общее использование входящего трафика пропускания
|
||||
# - '%outgoing-traffic-ttl%' Общее использование исходящего трафика пропускания
|
||||
# - '%used-memory%' Количество использованной памяти
|
||||
# - '%total-memory%' Количество общей памяти
|
||||
# - '%max-memory%' Объем максимальной памяти
|
||||
# - '%free-memory%' Количество свободной памяти
|
||||
# - '%animation%' Настраиваемый анимированный символ
|
||||
# Перевод статистики в режиме реального времени (verbose)
|
||||
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>На проверке <white>%verifying%
|
||||
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# Макет action bar, отображающей информацию во время атаки
|
||||
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<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%<reset>'
|
||||
# Анимация для action bar (текст с названием предмета)
|
||||
# Альтернативы:
|
||||
# - ▙, ▛, ▜, ▟
|
||||
# - ⬈, ⬊, ⬋, ⬉
|
||||
animation:
|
||||
- ◜
|
||||
- ◝
|
||||
- ◞
|
||||
- ◟
|
||||
|
||||
# Перевод сообщений об атаках
|
||||
notifications:
|
||||
# Layout for Sonar's attack notification title
|
||||
title: <yellow><b>Sonar<reset>
|
||||
# Макет для подзаголовка уведомления об атаке
|
||||
subtitle: <gray>Обнаружена бот-атака
|
||||
# Макет для уведомления об атаке в чате
|
||||
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,315 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Yardıma mı ihtiyacınız var veya sorularınız mı var? https://jonesdev.xyz/discord
|
||||
# Kaynak kodu: https://github.com/jonesdevelopment/sonar
|
||||
# Jones Development ve Sonar Katkıda Bulunanlar tarafından GPL-3.0 lisansı altında lisanslanmıştır
|
||||
#
|
||||
|
||||
# Bu yapılandırma dosyasındaki her '%prefix%' için yer tutucu
|
||||
prefix: <b><yellow>Sonar</b> <gray>» <white>
|
||||
# Bu yapılandırma dosyasındaki her '%support-url%' için yer tutucu
|
||||
support-url: https://jonesdev.xyz/discord/
|
||||
# Bu yapılandırma dosyasındaki her '%header%' için yer tutucu
|
||||
header:
|
||||
- <b><yellow>Sonar</b><reset>
|
||||
- ''
|
||||
# Bu yapılandırma dosyasındaki her '%footer%' için yer tutucu
|
||||
footer:
|
||||
- <gray>Bunun bir hata olduğunu düşünüyorsanız lütfen bir yönetici ile iletişime geçin.
|
||||
# Birisi bağlandığında ancak IP adresleriyle çevrimiçi olan oyuncu sayısı fazla olduğunda gösterilen bağlantı kesme mesajı
|
||||
too-many-online-per-ip:
|
||||
- '%header%'
|
||||
- <red>IP adresinizle çevrimiçi olan çok fazla oyuncu var.
|
||||
- '%footer%'
|
||||
|
||||
# Komutlar için çeviriler
|
||||
commands:
|
||||
# Bir oyuncu /sonar komutunu izinsiz çalıştırdığında gösterilen mesaj
|
||||
no-permission: '%prefix%<red>Bu komutu çalıştırmak için izniniz yok.'
|
||||
# Birisi bir komutu yanlış kullandığında gösterilen mesaj
|
||||
incorrect-usage: '%prefix%<red>Kullanım: /sonar %usage%'
|
||||
# Birisi geçersiz bir IP adresi sağladığında gösterilen mesaj (Geçersiz format)
|
||||
invalid-ip-address: '%prefix%Sağladığınız IP adresi geçersiz görünüyor.'
|
||||
# Birisi yasal olmayan bir IP adresi sağladığında gösterilen mesaj (Yerel IP)
|
||||
illegal-ip-address: '%prefix%Sağladığınız IP adresi yerel veya döngüsel bir IP gibi görünüyor.'
|
||||
# Konsol, sadece oyuncunun çalıştırabileceği bir komut çalıştırdığında gösterilen mesaj
|
||||
player-only: '%prefix%<red>Bu komutu sadece bir oyuncu olarak çalıştırabilirsiniz.'
|
||||
# Bir oyuncu, sadece konsolun çalıştırabileceği bir komutu çalıştırdığında gösterilen mesaj
|
||||
console-only: '%prefix%<red>Güvenlik nedeniyle, bu komutu sadece konsoldan çalıştırabilirsiniz.'
|
||||
# Bir oyuncu Sonar komutlarını çok hızlı çalıştırdığında gösterilen mesaj
|
||||
cool-down: '%prefix%<red>Bu komutu her 0,5 saniyede bir çalıştırabilirsiniz.'
|
||||
cool-down-left: '%prefix%<red>Lütfen bir <bold>%time-left%sn<reset><red> daha bekleyin.'
|
||||
# Bir oyuncunun belirli bir alt komutu çalıştırma izni olmadığında gösterilen mesaj
|
||||
subcommand-no-permission: '%prefix%<red>Bu alt komutu çalıştırmak için izniniz yok. <gray>(%permission%)'
|
||||
|
||||
# '/sonar' için çeviriler
|
||||
main:
|
||||
# Ana komutu çalıştırırken her şeyin üstünde gösterilen bilgilendirici mesaj
|
||||
header:
|
||||
- <yellow><b>Sonar</b> %version% sürümü %platform% üzerinde çalışıyor.
|
||||
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
|
||||
Jones Development ve Sonar Katkıda Bulunanlar</click>
|
||||
- ''
|
||||
- <gray>Yardıma mı ihtiyacınız var veya sorularınız mı var?
|
||||
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Discord\'u açmak için tıklayın)'>Discord üzerinden bir bilet açın </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(GitHub\'ı açmak için tıklayın)'>veya GitHub üzerinde yeni bir sorun açın.
|
||||
- ''
|
||||
# Ana komutu çalıştırırken gösterilen alt komutların listesi için biçimlendirme
|
||||
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Sadece oyuncular: </gray>%only_players%<br><gray>Konsol gerekli: </gray>%require_console%<br><gray>İzin: </gray><white>%permission%<br><gray>Diğer isimler: </gray>%aliases%''><dark_aqua>
|
||||
▪ <gray>/sonar %subcommand% <white>%description%'
|
||||
|
||||
# '/sonar reload' için çeviriler
|
||||
reload:
|
||||
# Birisi Sonar'ı yeniden yüklemeye başladığında gösterilen mesaj
|
||||
start: '%prefix%Sonar yeniden yükleniyor...'
|
||||
# Sonar yeniden yüklendiğinde gösterilen mesaj
|
||||
finish: '%prefix%<green>Sonar başarıyla yeniden yüklendi <gray>(%taken%ms)'
|
||||
|
||||
# '/sonar verbose' için çeviriler
|
||||
verbose:
|
||||
# Bir oyuncu Sonar detaylı bilgilerini görmeye başladığında gösterilen mesaj
|
||||
subscribed: '%prefix%Artık Sonar detaylı bilgilerini görüntülüyorsunuz.'
|
||||
# Bir oyuncu Sonar detaylı bilgilerinden çıktığında gösterilen mesaj
|
||||
unsubscribed: '%prefix%Artık Sonar detaylı bilgilerini görüntülemiyorsunuz.'
|
||||
|
||||
# '/sonar notify' için çeviriler
|
||||
notify:
|
||||
# Bir oyuncu Sonar saldırı bildirimlerini görmeye başladığında gösterilen mesaj
|
||||
subscribed: '%prefix%Artık Sonar saldırı bildirimlerini görüntülüyorsunuz.'
|
||||
# Bir oyuncu Sonar saldırı bildirimlerinden çıktığında gösterilen mesaj
|
||||
unsubscribed: '%prefix%Artık Sonar saldırı bildirimlerini görüntülemiyorsunuz.'
|
||||
|
||||
# '/sonar blacklist' için çeviriler
|
||||
blacklist:
|
||||
# Birisi blacklist'i temizlemeye çalıştığında ama boş olduğunda gösterilen mesaj
|
||||
empty: '%prefix%Kara liste şu anda boş. Bu nedenle, kara listeden hiçbir IP adresi kaldırılmadı.'
|
||||
# Birisi kara listeyi temizlediğinde gösterilen mesaj
|
||||
cleared: '%prefix%Toplamda %removed% IP adres(ler)i başarıyla kara listeden kaldırıldı.'
|
||||
# Birisi kara listenin boyutunu kontrol ettiğinde gösterilen mesaj
|
||||
size: '%prefix%Kara liste şu anda %amount% IP adres(ler)i içeriyor.'
|
||||
# Birisi bir IP adresini kara listeye eklediğinde gösterilen mesaj
|
||||
added: '%prefix%Başarıyla %ip% kara listeye eklendi.'
|
||||
# Birisi doğrulanmış olarak işaretlenmiş bir IP adresini kara listeye eklediğinde gösterilen mesaj
|
||||
added-warning: '%prefix%<red>Uyarı: <white>%ip% şu anda beyaz listede. Potansiyel sorunları önlemek için IP adresini doğrulanmış oyuncular listesinden kaldırmayı düşünün.'
|
||||
# Birisi bir IP adresini kara listeden çıkardığında gösterilen mesaj
|
||||
removed: '%prefix%Başarıyla %ip% kara listeden çıkarıldı.'
|
||||
# Birisi bir IP adresini kara listeye eklemeye çalıştığında ama zaten kara listede olduğunda gösterilen mesaj
|
||||
duplicate-ip: '%prefix%Sağladığınız IP adresi zaten kara listede.'
|
||||
# Birisi bir IP adresini kara listeden çıkarmaya çalıştığında ama kara listede olmadığında gösterilen mesaj
|
||||
ip-not-found: '%prefix%Sağladığınız IP adresi kara listede değil.'
|
||||
|
||||
# '/sonar verified' için çeviriler
|
||||
verified:
|
||||
# Birisi doğrulanmış oyuncular listesini temizlemeye çalıştığında ama boş olduğunda gösterilen mesaj
|
||||
empty: '%prefix%Doğrulanmış oyuncular listesi şu anda boş. Bu nedenle, hiçbir oyuncu doğrulanmamış olarak işaretlenmedi.'
|
||||
# Birisi doğrulanmış oyuncular listesini temizlediğinde gösterilen mesaj
|
||||
cleared: '%prefix%Toplamda %removed% benzersiz oyuncu başarıyla doğrulanmamış olarak işaretlendi.'
|
||||
# Birisi doğrulanmış oyuncular listesinin boyutunu kontrol ettiğinde gösterilen mesaj
|
||||
size: '%prefix%Şu anda %amount% benzersiz oyuncu doğrulanmış.'
|
||||
# Birisi bir IP adresini doğrulanmamış olarak işaretlediğinde gösterilen mesaj
|
||||
removed: '%prefix%Başarıyla %ip% doğrulanmamış olarak işaretlendi.'
|
||||
# Birisi bir IP adresini doğrulanmamış olarak işaretlemeye çalıştığında ama doğrulanmamış olduğunda gösterilen mesaj
|
||||
ip-not-found: '%prefix%Sağladığınız IP adresi doğrulanmış olarak işaretlenmemiş.'
|
||||
# Birisi aynı IP adresini iki kez (çift işlem) doğrulanmamış olarak işaretlemeye çalıştığında gösterilen mesaj
|
||||
blocked: '%prefix%Mevcut işlemin bitmesini bekleyin.'
|
||||
|
||||
# '/sonar statistics' için çeviriler
|
||||
statistics:
|
||||
# İstatistikleri görüntülerken her şeyin üzerinde gösterilen bilgilendirme mesajı
|
||||
header: 'Bu oturum için %type% istatistikleri gösteriliyor:'
|
||||
# Message that is shown when a player tries viewing an unknown statistic
|
||||
unknown-type: '%prefix%<red>Bilinmeyen istatistik türü! Mevcut istatistikler: <gray>%statistics%'
|
||||
# Genel istatistik mesajının biçimi
|
||||
general:
|
||||
- ' <dark_aqua>▪ <gray>Doğrulanmış IP adresleri: <white>%verified%'
|
||||
- ' <dark_aqua>▪ <gray>Doğrulanan IP adresleri: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Kara listedeki IP adresleri: <white>%blacklisted%'
|
||||
- ' <dark_aqua>▪ <gray>Şu anda sıraya alınmış girişler: <white>%queued%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam benzersiz olmayan girişler: <white>%total_joins%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam doğrulama denemeleri: <white>%total_attempts%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam başarısız doğrulama denemesi: <white>%total_failed%'
|
||||
- ' <dark_aqua>▪ <gray>Sunucu çalışma süresi: <white>%uptime%'
|
||||
# İşlemci istatistik mesajının biçimi
|
||||
cpu:
|
||||
- ' <dark_aqua>▪ <gray>Şu anda işlemci CPU kullanımı: <white>%process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Şu anda sistem CPU kullanımı: <white>%system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Çekirdek başına işlem CPU kullanımı: <white>%average_process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Çekirdek başına sistem CPU kullanımı: <white>%average_system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Genel sistem yük ortalaması: <white>%load_average%%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam sanal CPU miktarı: <white>%virtual_cores%'
|
||||
# Hafıza istatistik mesajının biçimi
|
||||
memory:
|
||||
- ' <dark_aqua>▪ <gray>Toplam boş hafıza: <white>%free_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam kullanılan hafıza: <white>%used_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam maksimum bellek: <white>%max_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam ayrılmış bellek: <white>%total_memory%'
|
||||
# İnternet istatistik mesajının biçimi
|
||||
network:
|
||||
- ' <dark_aqua>▪ <gray>Mevcut gelen kullanılan bant genişliği: <white>%incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Mevcut giden kullanılan bant genişliği: <white>%outgoing%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam gelen kullanılan bant genişliği: <white>%ttl_incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Toplam giden kullanılan bant genişliği: <white>%ttl_outgoing%'
|
||||
|
||||
# Doğrulama işlemi sırasında tüm mesajlar için çeviriler
|
||||
verification:
|
||||
# Doğrulama sırasında tüm hata ayıklama mesajları için çeviriler
|
||||
logs:
|
||||
# Sunucuya yeni bir oyuncu katıldığında konsola kaydedilen mesaj
|
||||
connection: '%name%%ip% (%protocol%) sunucuya bağlandı.'
|
||||
# Bir oyuncu doğrulamada başarısız olduğunda konsola kaydedilen mesaj
|
||||
failed: '%ip% (%protocol%) bot korumasında başarısız oldu: %reason%'
|
||||
# Bir oyuncu kara listeye alındığında konsola kaydedilen mesaj
|
||||
blacklisted: '%ip% (%protocol%) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
|
||||
# Bir oyuncu doğrulandığında konsola kaydedilen mesaj
|
||||
successful: '%name% başarıyla doğrulandı! (%time%s).'
|
||||
|
||||
# Yerçekimi için kontrol edildiklerinde oyuncuya gösterilen mesaj
|
||||
welcome: '%prefix%<gray>Lütfen doğrulamanın bitmesini bekleyin...'
|
||||
|
||||
# CAPTCHA'yı girmeleri gerektiğinde oyuncuya gösterilen mesajlar
|
||||
captcha:
|
||||
# Captcha'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
|
||||
enter-code: '%prefix%<green>Haritada gösterilen kodu lütfen sohbete girin.'
|
||||
# Timer that is shown to the player when they have to enter the answer to the captcha
|
||||
# (Set this to '' to disable the action bar message)
|
||||
action-bar: '%prefix%<green>Kodu girmek için %time-left% saniyeniz kaldı'
|
||||
# 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ı
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>Anti-bot verileriniz henüz hazır değil.
|
||||
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
|
||||
- '%footer%'
|
||||
# Aynı anda çok fazla oyuncu doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
|
||||
too-many-players:
|
||||
- '%header%'
|
||||
- <gold>Too many players are currently trying to log in, try again later.
|
||||
- <gray>Please wait a few seconds before trying to join again.
|
||||
- '%footer%'
|
||||
# Doğrulama sırasında birisi çok hızlı bir şekilde yeniden katıldığında gösterilen bağlantıyı kesme mesajı
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>Çok hızlı yeniden bağlandın.
|
||||
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
|
||||
- '%footer%'
|
||||
# Birisi katıldığında ancak zaten doğrulama yaparken gösterilen bağlantıyı kesme mesajı
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>IP adresiniz şu anda doğrulanıyorç
|
||||
- <red>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
|
||||
- '%footer%'
|
||||
# Birisi katıldığında gösterilen ancak doğrulama için zaten sıraya alınmış olduğunda gösterilen bağlantıyı kesme mesajı
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>IP adresiniz şu an doğrulanma sırasında.
|
||||
- <red>Tekrar doğrulamayı denemeden önce lütfen birkaç dakika bekleyin.
|
||||
- '%footer%'
|
||||
# Birisi katıldığında ancak geçici olarak kara listeye alındığında gösterilen bağlantıyı kesme mesajı
|
||||
blacklisted:
|
||||
- '%header%'
|
||||
- <red>Şu anda sunucuya girişiniz engellenmiştir.
|
||||
- <red>Sunucuya tekrar katılabilmek için lütfen birkaç dakika bekleyin.
|
||||
- <gold>Bunun bir hata olduğunu mu düşünüyorsunuz? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Birisi geçersiz bir kullanıcı adıyla katıldığında gösterilen bağlantıyı kesme mesajı
|
||||
invalid-username:
|
||||
- '%header%'
|
||||
- <red>Kullanıcı adınız geçersiz karakterler içeriyor.
|
||||
- '%footer%'
|
||||
# Birisi kara listeye alınmış bir sürümle katıldığında gösterilen bağlantıyı kes mesajı
|
||||
blacklisted-protocol:
|
||||
- '%header%'
|
||||
- <red>Sunucumuzda izin verilmeyen bir sürüm ile giriş yapmaya çalışıyorsun.
|
||||
- <gold>Giriş yapma hakkında yardıma mı ihtiyacın var? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Birisi çevrimiçi olan bir hesapla doğrulama yapmaya çalıştığında gösterilen bağlantıyı kesme mesajı
|
||||
already-online:
|
||||
- '%header%'
|
||||
- <red>Hesabınızla zaten çevrimiçi olan biri var.
|
||||
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
|
||||
- <gray>Bu durum devam ederse, oyununuzu yeniden başlatmayı deneyin veya destek ekibiyle iletişime geçin.
|
||||
- '%footer%'
|
||||
# Birisi başarıyla doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
|
||||
success:
|
||||
- '%header%'
|
||||
- <green>Doğrulamayı başarıyla geçtin.
|
||||
- <white>Yeniden bağlandığınızda artık sunucuda oynayabilirsiniz.
|
||||
# Birisi doğrulamayı başaramadığında gösterilen bağlantıyı kesme mesajı
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>Doğrulamayı geçemediniz.
|
||||
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
|
||||
- <gold>Giriş yapma hakkında yardıma mı ihtiyacın var? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
# Sonar'ın actionbar verbose mesajları için çeviriler
|
||||
verbose:
|
||||
layout:
|
||||
# Sonar verbose için yer tutucular
|
||||
# Placeholders:
|
||||
# - '%queued%' Sırada bekleyen bağlantı sayısı
|
||||
# - '%verifying%' Doğrulanıyor olan bağlantı sayısı
|
||||
# - '%blacklisted%' Kara listeye alınmış IP adresi sayısı
|
||||
# - '%total-joins%' Toplam denenen katılım sayısı
|
||||
# - '%verify-total%' Toplam doğrulama denemesi sayısı
|
||||
# - '%verify-success%' Doğrulanmış IP adresi sayısı
|
||||
# - '%verify-failed%' Başarısız doğrulama sayısı
|
||||
# - '%logins-per-second%' Saniye başına giriş sayısı
|
||||
# - '%connections-per-second%' Saniye başına bağlantı sayısı
|
||||
# - '%attack-duration%' Şu anki bağlantının süresi
|
||||
# - '%incoming-traffic%': Saniyedeki gelen bant genişliği kullanımı
|
||||
# - '%outgoing-traffic%': Saniyedeki giden bant genişliği kullanımı
|
||||
# - '%incoming-traffic-ttl%': Toplam gelen bant genişliği kullanımı
|
||||
# - '%outgoing-traffic-ttl%': Toplam giden bant genişliği kullanımı
|
||||
# - '%used-memory%' İşlemin kullanılan bellek miktarı
|
||||
# - '%total-memory%' İşlemin toplam bellek miktarı
|
||||
# - '%max-memory%' İşlemin maksimum bellek miktarı
|
||||
# - '%free-memory%' İşlemin boş bellek miktarı
|
||||
# - '%animation%' Özelleştirilebilir animasyonlu simge
|
||||
# Translations for Sonar's normal verbose output
|
||||
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Girişler/sn
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>Doğrulama <white>%verifying% <dark_aqua>|
|
||||
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# Layout for Sonar's actionbar verbose during an attack
|
||||
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Girişler/sn
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>Sıra <white>%queued% <dark_aqua>╺
|
||||
<gray>Doğrulama <white>%verifying% <dark_aqua>╺ <gray>Saldırı süresi <white>%attack-duration%
|
||||
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# 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>Bir bot saldırısı tespit edildi
|
||||
# Layout for Sonar's attack notification in chat
|
||||
chat:
|
||||
- ''
|
||||
- <yellow><b>Sonar<reset>
|
||||
- ''
|
||||
- <gray>Bir bot saldırısı tespit edildi.
|
||||
- ' <dark_aqua>▪ <gray>Saniye başına giriş sayısı: <white>%logins-per-second%'
|
||||
- ' <dark_aqua>▪ <gray>Doğrulanıyor olan oyuncular: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Sırada bekleyen oyuncular: <white>%queued%'
|
||||
- ''
|
||||
- <gray>Daha fazla bilgi görmek için <white><click:run_command:'/sonar verbose'><hover:show_text:'(Çalıştırmak için tıkla)'>/sonar verbose</hover></click><gray> veya <white><click:run_command:'/sonar
|
||||
stats'><hover:show_text:'(Çalıştırmak için tıkla)'>/sonar stats</hover></click><gray> kullanın.
|
||||
- ''
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
|
||||
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development a Sonar Contributors, licencováno pod GPL-3.0
|
||||
#
|
||||
|
||||
# Notifikace útoků můžou být poslány na discord server pomocí webhooků
|
||||
webhook:
|
||||
# URL Discord webhooku (Ponechejte '' pro žádné webhooky)
|
||||
url: ''
|
||||
# Jméno discord webhooku
|
||||
username: Sonar
|
||||
# URL pro avatara discord webhooku (Ponechejte na '' pro žádného)
|
||||
avatar-url: ''
|
||||
# Obsah zprávy webhooku (Ponechte na '' pro žadnou)
|
||||
# Můžete to použít k např. upozorvování členů staff týmu pomocí <@userId>
|
||||
# Pokuď budete chtít upozornit role, budete muset použít <@&roleId>
|
||||
content: ''
|
||||
# Přidejte zprávu pro webhook, kterou ma poslat když skončil útok
|
||||
embed:
|
||||
# Malé zápatí pro poslanou zprávu webhookem
|
||||
footer:
|
||||
# Obsah zápatí kterou discord webhooku
|
||||
text: © Jones Development and Sonar Contributors
|
||||
# URL ikony zápatí zprávy
|
||||
icon-url: ''
|
||||
# Nadpis discord webhook zprávy
|
||||
title: ':white_check_mark: Útok zahájen'
|
||||
# URL, na kterou se bude moct na webhook zprávě kliknout
|
||||
title-url: ''
|
||||
# Popisek (kontext) discord webhook zprávy
|
||||
description:
|
||||
- Útok na váš server byl ukončen!.
|
||||
- ''
|
||||
- 'Začátek útoku: <t:%start-timestamp%:T>'
|
||||
- 'Konec útoku: <t:%end-timestamp%:T>'
|
||||
- 'Délka útoku: %duration%'
|
||||
- ''
|
||||
- 'Nejvyšší využití CPU při útoku: %peak-cpu%%'
|
||||
- 'Nejvyšší využití paměti při útoku: %peak-memory%'
|
||||
- 'Nejvyšší počet botů za sekundu při útoku: %peak-bps%'
|
||||
- ''
|
||||
- 'IP přidány na černou listinu: %total-blacklisted%'
|
||||
- 'Nepovedené verifikace při útoku: %total-failed%'
|
||||
- 'Povedené verifikace při útoku: %total-success%'
|
||||
# RGB barvy discord webhook zprávy
|
||||
# Barvy: https://www.rapidtables.com/web/color/RGB_Color.html
|
||||
color:
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 0
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Need help or have questions? 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: Attack mitigated'
|
||||
# Clickable URL of the title of the Discord webhook embed
|
||||
title-url: ''
|
||||
# Description (content) of the Discord webhook embed
|
||||
description:
|
||||
- The attack on your server has been mitigated.
|
||||
- ''
|
||||
- 'Attack start: <t:%start-timestamp%:T>'
|
||||
- 'Attack end: <t:%end-timestamp%:T>'
|
||||
- 'Attack duration: %duration%'
|
||||
- ''
|
||||
- 'Peak process CPU usage during the attack: %peak-cpu%%'
|
||||
- 'Peak process memory usage during the attack: %peak-memory%'
|
||||
- 'Peak bots per second during the attack: %peak-bps%'
|
||||
- ''
|
||||
- 'Blacklisted IP addresses during the attack: %total-blacklisted%'
|
||||
- 'Failed verifications during the attack: %total-failed%'
|
||||
- 'Successful verifications during the attack: %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
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# დახმარება გჭირდება ან კითხვა გაქვს? https://jonesdev.xyz/discord
|
||||
# საწყისი კოდი: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# შემოტევასთან დაკავშირებული მესიჯების გაგზავნა თქვენს Discord სერვერში შეიძლება ვებჰუკების მეშვეობით
|
||||
webhook:
|
||||
# ვებჰუკის ლინკი (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
|
||||
url: ''
|
||||
# სახელი ვებჰუკის გამგზავნელის
|
||||
username: Sonar
|
||||
# ლინკი ავატარის, რომელიც იქნება ვებჰუკის ავატარად გამოყენებული (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
|
||||
avatar-url: ''
|
||||
# ვებჰუკის შეტყობინების შინაარსი (დააყენეთ ეს '' თუ გინდათ, რომ გათიშოთ ეს ფუნქცია)
|
||||
# თქვენ შეგიძლიათ ეს გამოიყენოთ, რომ მაგალითად მოთაგოთ სტაფის წევრი <@userId>
|
||||
# თუ გინდათ, რომ მოთაგოთ როლი, გამოიყენეთ ეს <@&roleId>
|
||||
content: ''
|
||||
# განათავსეთ ვებჰუკის შეტყობინება, რომელიც იგზავნება შეტევის შეჩერებისას
|
||||
embed:
|
||||
# ვებჰუკის შეტყობინების ქვედა მცირე ტექსტი
|
||||
footer:
|
||||
# ვებჰუკის ქვედა მცირე შეტყობინების შინაარსი
|
||||
text: © Jones Development and Sonar Contributors
|
||||
# ლინკი ქვედა მცირე შეტყობინობის ავატარისთვის
|
||||
icon-url: ''
|
||||
# ვებჰუკის სათაური
|
||||
title: ':white_check_mark: შეტევა გადატანილია'
|
||||
# ლინკი რომელზეც გადავნენ ვებჰუკის სათაურზე დაჭერის შემდეგ
|
||||
title-url: ''
|
||||
# აღწერა (ტექსტი) ვებჰუკის მესიჯის
|
||||
description:
|
||||
- თქვენს სერვზე განხორციელებული შემოტევა წარმატებულად იყო გადატანილი.
|
||||
- ''
|
||||
- 'შემოტევა დაიწყო: <t:%start-timestamp%:T>'
|
||||
- 'შემოტევა დასრულდა: <t:%end-timestamp%:T>'
|
||||
- 'შემოტევის ხანგრძლივობა: %duration%'
|
||||
- ''
|
||||
- 'CPU-ის გამოყენების პიკი შეტევის დროს: %peak-cpu%%'
|
||||
- 'RAM-ის გამოყენების პიკი შეტევის დროს: %peak-memory%'
|
||||
- 'ბოტების პიკი წამში შეტევის დროს: %peak-bps%'
|
||||
- ''
|
||||
- 'შეტევის დროს შავ სიაში IP მისამართები: %total-blacklisted%'
|
||||
- 'შეტევის დროს ვერიფიკაცია ვერ შეასრულა: %total-failed%'
|
||||
- 'წარმატებულად ვერიფირებული თავდასხმის დროს: %total-success%'
|
||||
# RGB ფერები ვებჰუკის მესიჯისთვის
|
||||
# Color picker: https://www.rapidtables.com/web/color/RGB_Color.html
|
||||
color:
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 0
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
|
||||
# Исходный код: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# Уведомления о бот-атаках могут быть отправлены на ваш сервер Discord с помощью вебхуков
|
||||
webhook:
|
||||
# URL вебхука Discord (Установите значение '' чтобы отключить вебхук)
|
||||
url: ''
|
||||
# Ник вебхука Discord
|
||||
username: Sonar
|
||||
# URL аватара вебхука Discord (Установите значение '' чтобы не использовать аватар)
|
||||
avatar-url: ''
|
||||
# Контент сообщения вебхука Discord (Установите значение '' чтобы не использовать контент)
|
||||
# Вы можете использовать это для оповещения персонала, например: <@userId>
|
||||
# Если вы хотите пинговать роли, вам нужно использовать <@&roleId>
|
||||
content: ''
|
||||
# Embed сообщение, отправляемое после прекращения атаки
|
||||
embed:
|
||||
# Нижний текст в панельном сообщении
|
||||
footer:
|
||||
# Содержимое нижнего текста в панельном сообщении
|
||||
text: © Jones Development and Sonar Contributors
|
||||
# Иконка нижнего текста в панельном сообщении
|
||||
icon-url: ''
|
||||
# Заголовок панельного сообщения
|
||||
title: ':white_check_mark: Attack mitigated'
|
||||
# Кликабельный URL-адрес заголовка
|
||||
title-url: ''
|
||||
# Описание (контент) панельного сообщения
|
||||
description:
|
||||
- Атака на ваш сервер была устранена.
|
||||
- ''
|
||||
- 'Начало атаки: <t:%start-timestamp%:T>'
|
||||
- 'Конец атаки: <t:%end-timestamp%:T>'
|
||||
- 'Продолжительность атаки: %duration%'
|
||||
- ''
|
||||
- 'Пиковое использование процессора во время атаки: %peak-cpu%%'
|
||||
- 'Пиковое использование памяти во время атаки: %peak-memory%'
|
||||
- 'Пиковая скорость входа ботов в секунду во время атаки: %peak-bps%'
|
||||
- ''
|
||||
- 'IP-адреса, внесенные в черный список во время атаки: %total-blacklisted%'
|
||||
- 'Проваленные проверки во время атаки: %total-failed%'
|
||||
- 'Успешные проверки во время атаки: %total-success%'
|
||||
# Цвета RGB используемые в панельном сообщении
|
||||
# Выбор цвета: https://www.rapidtables.com/web/color/RGB_Color.html
|
||||
color:
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 0
|
||||
@@ -9,6 +9,9 @@ dependencies {
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-velocity:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-velocity:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -17,24 +17,29 @@
|
||||
|
||||
package xyz.jonesdev.sonar.velocity;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.alessiodp.libby.VelocityLibraryManager;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.injection.BaseChannelInitializer;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
public static SonarVelocity INSTANCE;
|
||||
|
||||
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
|
||||
super(plugin, plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
|
||||
super(plugin, new VelocityLibraryManager<>(
|
||||
plugin, plugin.getLogger(), plugin.getDataDirectory(), plugin.getServer().getPluginManager()),
|
||||
plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -72,13 +77,27 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener());
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject new channel initializer
|
||||
final VelocityServer server = (VelocityServer) getPlugin().getServer();
|
||||
BaseInjectionHelper.inject(server, new BaseChannelInitializer(server));
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Bandwidth::reset)
|
||||
.repeat(1L, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue().getPollTask())
|
||||
.repeat(500L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Sonar.get().getVerboseHandler()::update)
|
||||
.repeat(200L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject(getPlugin().getServer());
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -28,11 +28,11 @@ public final class AudienceListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getUsername(), event.getPlayer());
|
||||
Sonar.AUDIENCES.put(event.getPlayer().getUsername(), event.getPlayer());
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull DisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getUsername());
|
||||
Sonar.AUDIENCES.remove(event.getPlayer().getUsername());
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||
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 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.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import static com.velocitypowered.proxy.network.Connections.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Channel channel;
|
||||
private @Nullable String username;
|
||||
private InetAddress inetAddress;
|
||||
private ProtocolVersion protocolVersion;
|
||||
private HandshakePacket handshakePacket;
|
||||
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
@Override
|
||||
public void channelActive(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Increase connections per second for the action bar verbose
|
||||
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final @NotNull ChannelHandlerContext ctx) {
|
||||
// The player can disconnect without sending the login packet first
|
||||
if (username != null) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
}
|
||||
// The player cannot be in the queue if the IP address is invalid
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any handshake packet by the client
|
||||
if (msg instanceof HandshakePacket handshake) {
|
||||
handleHandshake(handshake);
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (msg instanceof ServerLoginPacket serverLogin) {
|
||||
handleLogin(ctx, serverLogin);
|
||||
return;
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
// We can override the default exceptionCaught method since this handler
|
||||
// will run before the MinecraftConnection knows that there has been an error.
|
||||
// Additionally, this will also run after our custom decoder.
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Throwable cause) throws Exception {
|
||||
// Sonar sometimes uses exceptions to quickly close
|
||||
// the channel and interrupt any other ongoing process.
|
||||
// Simply close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull HandshakePacket handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (handshakePacket != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getServerAddress().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion().getProtocol());
|
||||
// Store the handshake packet
|
||||
handshakePacket = handshake;
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ServerLoginPacket serverLogin) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
// Store the username
|
||||
username = serverLogin.getUsername();
|
||||
// Make sure to use the potentially modified, original IP
|
||||
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getBlacklisted(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(serverLogin.getUsername())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final GameProfile gameProfile = GameProfile.forOfflinePlayer(serverLogin.getUsername());
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(gameProfile.getName()).matches()) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getVerification().getInvalidUsername(),
|
||||
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(
|
||||
gameProfile.getName(), gameProfile.getId(), MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static 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 Method INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private final ChannelInitializer<Channel> originalChannelInitializer;
|
||||
|
||||
@Override
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
// Invoke the original method
|
||||
try {
|
||||
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
channel.pipeline().addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.network.ServerChannelInitializerHolder;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackInjectionHelper {
|
||||
private final Field CONNECTION_MANAGER_FIELD;
|
||||
private final Field SERVER_CHANNEL_INITIALIZER_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CONNECTION_MANAGER_FIELD = VelocityServer.class.getDeclaredField("cm");
|
||||
CONNECTION_MANAGER_FIELD.setAccessible(true);
|
||||
|
||||
SERVER_CHANNEL_INITIALIZER_FIELD = ServerChannelInitializerHolder.class.getDeclaredField("initializer");
|
||||
SERVER_CHANNEL_INITIALIZER_FIELD.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void inject(final ProxyServer 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);
|
||||
|
||||
// Replace the original channel initializer
|
||||
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), fallbackInitializer);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-14
@@ -19,7 +19,10 @@ package xyz.jonesdev.sonar.velocity.fallback;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.event.connection.LoginEvent;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.velocity.SonarVelocity;
|
||||
@@ -27,25 +30,28 @@ import xyz.jonesdev.sonar.velocity.SonarVelocity;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FallbackListener {
|
||||
public final class FallbackLoginListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PreLoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getPlayer().getRemoteAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getRemoteAddress().getAddress(), inetAddress))
|
||||
.count();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getRemoteAddress().getAddress(), inetAddress))
|
||||
.count();
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Sonar.get().getConfig().getTooManyOnlinePerIp()));
|
||||
}
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) event.getPlayer();
|
||||
connectedPlayer.getConnection().closeWith(DisconnectPacket.create(
|
||||
Sonar.get().getConfig().getTooManyOnlinePerIp(),
|
||||
connectedPlayer.getProtocolVersion(), StateRegistry.LOGIN));
|
||||
}
|
||||
}
|
||||
}
|
||||
-315
@@ -1,315 +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 com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Counters;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static com.velocitypowered.proxy.network.Connections.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackMinecraftConnection extends MinecraftConnection {
|
||||
public FallbackMinecraftConnection(final Channel channel, final VelocityServer server) {
|
||||
super(channel, server);
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
private final Channel channel;
|
||||
private FallbackUserWrapper user;
|
||||
private boolean receivedLoginPacket;
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
private static final boolean FORCE_SECURE_PROFILES;
|
||||
|
||||
static {
|
||||
FORCE_SECURE_PROFILES = Boolean.getBoolean("auth.forceSecureProfiles");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
super.channelActive(ctx);
|
||||
// Increase connections per second for the action bar verbose
|
||||
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
if (msg instanceof Handshake handshake) {
|
||||
// Check if the host is invalid
|
||||
if (handshake.getServerAddress().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
}
|
||||
if (msg instanceof ServerLogin login) {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new CorruptedFrameException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (isClosed() || isKnownDisconnect()) return;
|
||||
|
||||
try {
|
||||
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
|
||||
|
||||
final InetAddress inetAddress = ((InetSocketAddress) getRemoteAddress()).getAddress();
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress)) {
|
||||
// Mark the connection as dead to avoid unnecessary console logs
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getBlacklisted(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
login.getUsername(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
final int protocolId = getProtocolVersion().getProtocol();
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
// Mark the connection as dead to avoid unnecessary console logs
|
||||
//markConnectionAsDead(activeSessionHandler);
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already verified.
|
||||
// No one wants to be verified over and over again.
|
||||
final GameProfile gameProfile = GameProfile.forOfflinePlayer(login.getUsername());
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow exceptions or disconnect messages
|
||||
setAssociation(null);
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getAlreadyQueued(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(login.getUsername())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(login.getUsername(), inetAddress));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (isClosed() || isKnownDisconnect()) 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(login.getUsername()).matches()) {
|
||||
closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(READ_TIMEOUT, READ_TIMEOUT, new FallbackTimeoutHandler(readTimeout,
|
||||
TimeUnit.MILLISECONDS));
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(FALLBACK, this, channel, channel.pipeline(),
|
||||
inetAddress, ProtocolVersion.fromId(protocolId));
|
||||
|
||||
// Perform default Velocity checks
|
||||
final IdentifiedKey playerKey = login.getPlayerKey();
|
||||
if (playerKey != null) {
|
||||
if (playerKey.hasExpired()) {
|
||||
closeWith(Disconnect.create(
|
||||
Component.translatable("multiplayer.disconnect.invalid_public_key_signature"),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isKeyValid;
|
||||
if (playerKey.getKeyRevision() == IdentifiedKey.Revision.LINKED_V2 && playerKey instanceof IdentifiedKeyImpl keyImpl) {
|
||||
isKeyValid = keyImpl.internalAddHolder(login.getHolderUuid());
|
||||
} else {
|
||||
isKeyValid = playerKey.isSignatureValid();
|
||||
}
|
||||
|
||||
if (!isKeyValid) {
|
||||
closeWith(Disconnect.create(
|
||||
Component.translatable("multiplayer.disconnect.invalid_public_key"),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
} else if ((FORCE_SECURE_PROFILES || server.getConfiguration().isForceKeyAuthentication())
|
||||
&& getProtocolVersion().compareTo(MINECRAFT_1_19) >= 0
|
||||
&& getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
closeWith(Disconnect.create(
|
||||
Component.translatable("multiplayer.disconnect.missing_public_key"),
|
||||
getProtocolVersion(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
if (server.getPlayer(gameProfile.getId()).isPresent()
|
||||
|| server.getPlayer(gameProfile.getName()).isPresent()) {
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getAlreadyConnected());
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", login.getUsername())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(gameProfile.getName(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(MINECRAFT_ENCODER) == null
|
||||
|| user.getPipeline().get(MINECRAFT_DECODER) == null) {
|
||||
close(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, gameProfile.getName(), gameProfile.getId())));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
}
|
||||
-60
@@ -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 com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = {"protocolVersion"})
|
||||
public final class FallbackUserWrapper implements FallbackUser<MinecraftConnection> {
|
||||
private final Fallback fallback;
|
||||
private final MinecraftConnection connection;
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final @NotNull Object packet) {
|
||||
connection.write(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delayedWrite(final @NotNull Object packet) {
|
||||
connection.delayedWrite(packet);
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +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.injection;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
||||
import com.velocitypowered.proxy.network.Connections;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.netty.*;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackMinecraftConnection;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.velocitypowered.proxy.network.Connections.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private final VelocityServer server;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java
|
||||
@Override
|
||||
protected void initChannel(final @NotNull Channel channel) {
|
||||
channel.pipeline()
|
||||
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
|
||||
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
|
||||
.addLast(READ_TIMEOUT,
|
||||
new FallbackTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS))
|
||||
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
|
||||
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
|
||||
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.SERVERBOUND))
|
||||
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND));
|
||||
|
||||
final MinecraftConnection connection = new FallbackMinecraftConnection(channel, server);
|
||||
connection.setActiveSessionHandler(StateRegistry.HANDSHAKE,
|
||||
new HandshakeSessionHandler(connection, server));
|
||||
channel.pipeline().addLast(Connections.HANDLER, connection);
|
||||
|
||||
if (server.getConfiguration().isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.fallback.injection;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
@UtilityClass
|
||||
public class BaseInjectionHelper {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public void inject(final VelocityServer server, final ChannelInitializer<Channel> serverInitializer) {
|
||||
try {
|
||||
final Field field = VelocityServer.class.getDeclaredField("cm");
|
||||
field.setAccessible(true);
|
||||
|
||||
final ConnectionManager cm = (ConnectionManager) field.get(server);
|
||||
|
||||
cm.serverChannelInitializer.set(serverInitializer);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,23 @@
|
||||
"Jones Development",
|
||||
"Sonar Contributors"
|
||||
],
|
||||
"dependencies": [],
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "geyser",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "floodgate",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "protocolize",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"id": "viaversion",
|
||||
"optional": true
|
||||
}
|
||||
],
|
||||
"main": "$main"
|
||||
}
|
||||
Reference in new issue
Block a user