Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5068ebcd91 | ||
|
|
f20d162dcc | ||
|
|
acc9df0b8d | ||
|
|
3636674eff | ||
|
|
d2b3b0adae | ||
|
|
194c7645ed | ||
|
|
860594400a | ||
|
|
20f5b135aa | ||
|
|
ef31dd4c87 | ||
|
|
8361f1990d | ||
|
|
bf9e77a437 | ||
|
|
7f604dd4ad | ||
|
|
ab96929070 | ||
|
|
fa20826b8e | ||
|
|
c8c212a3ee | ||
|
|
ad7d3e62eb | ||
|
|
343d71d18c | ||
|
|
a2e252aece | ||
|
|
124bed86dc | ||
|
|
971d43df00 | ||
|
|
1671ccac55 | ||
|
|
8e03bdac5f | ||
|
|
7c0bdc568e | ||
|
|
354c7a0143 | ||
|
|
bb33aa3fa4 | ||
|
|
a0e308321d | ||
|
|
71b6d39eab | ||
|
|
3faf8c8b7f | ||
|
|
b45626eafb | ||
|
|
702ea1b370 | ||
|
|
a75c409e13 | ||
|
|
afe9f3f9ea | ||
|
|
ba63e80fd0 | ||
|
|
5ff7e23fcb | ||
|
|
cc194b0d62 | ||
|
|
4e4423e2ce | ||
|
|
ddb65f2f5a | ||
|
|
254faecc90 | ||
|
|
582024eba1 | ||
|
|
e1fe738ffe | ||
|
|
5f3a439113 | ||
|
|
27cca23412 | ||
|
|
2d2c576ad4 | ||
|
|
0adf7b0b6a | ||
|
|
c6ff9370c2 | ||
|
|
e33aac29c5 | ||
|
|
42dc3af6c9 | ||
|
|
ff402d5096 | ||
|
|
06a72bf3bd | ||
|
|
3fb4291163 | ||
|
|
cc57b1d978 | ||
|
|
750b42b5ab | ||
|
|
ad017024ae | ||
|
|
5887d82c19 | ||
|
|
b1e5d01062 | ||
|
|
a50bf45176 | ||
|
|
e0b5ad2a33 | ||
|
|
e5ee94362c | ||
|
|
d877568b38 | ||
|
|
0edec77135 | ||
|
|
54445cdfdb | ||
|
|
8545d60791 | ||
|
|
11327e2890 | ||
|
|
ad511836f3 | ||
|
|
5cb79d3acf | ||
|
|
032dd4f94e | ||
|
|
e831c1a646 | ||
|
|
f50c90a690 | ||
|
|
1e7e4ba5f1 |
No files matched your search
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
contact@jonesdev.xyz.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+3
-3
@@ -42,8 +42,8 @@
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
|
||||
all types of bots.
|
||||
- Spigot & BungeeCord
|
||||
- The compression method is changed
|
||||
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
|
||||
- Duplicate login/status packet spam fix
|
||||
- Run server ping handling asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
|
||||
@@ -91,4 +91,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
|
||||
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+27
-14
@@ -12,12 +12,9 @@ plugins {
|
||||
|
||||
apply(plugin = "io.toolebox.git-versioner")
|
||||
|
||||
rootProject.group = "xyz.jonesdev.sonar"
|
||||
val semanticVersion = "2.0.0"
|
||||
|
||||
versioner {
|
||||
pattern {
|
||||
pattern = "$semanticVersion-%h(-%c)"
|
||||
pattern = "$version-%h-%c-%b"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +22,7 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
|
||||
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity API
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
}
|
||||
}
|
||||
@@ -35,17 +32,24 @@ subprojects {
|
||||
apply(plugin = "com.github.johnrengelman.shadow")
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.projectlombok:lombok:1.18.28")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.28")
|
||||
compileOnly("org.projectlombok:lombok:1.18.30")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.28")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.28")
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.30")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.97.Final") // netty
|
||||
val adventureVersion = "4.14.0"
|
||||
|
||||
// adventure
|
||||
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
|
||||
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:$adventureVersion")
|
||||
|
||||
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,14 +66,23 @@ tasks {
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Version"] = version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception.
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.bstats")
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}.jar")
|
||||
archiveFileName.set("Sonar.jar")
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.7
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "Sonar"
|
||||
rootProject.name = "sonar"
|
||||
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
val path = "sonar-$it"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
dependencies {
|
||||
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") // yaml config
|
||||
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
|
||||
exclude(group = "org.yaml")
|
||||
}
|
||||
|
||||
compileOnly("org.jetbrains:annotations:24.0.1")
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackStatus;
|
||||
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.event.SonarEventManager;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
@@ -33,10 +36,14 @@ public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
|
||||
|
||||
Gson GENERAL_GSON = new GsonBuilder()
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the server
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@NotNull ServerWrapper getServer();
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
@@ -53,6 +60,12 @@ public interface Sonar {
|
||||
|
||||
@NotNull Verbose getVerboseHandler();
|
||||
|
||||
/**
|
||||
* Set a custom verbose handler
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setVerboseHandler(final @NotNull Verbose verboseHandler);
|
||||
|
||||
/**
|
||||
* Reloads the entire plugin
|
||||
*/
|
||||
@@ -63,10 +76,21 @@ public interface Sonar {
|
||||
return SonarVersion.GET;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default SonarEventManager getEventManager() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
return SonarEventManager.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default AttackStatus getAttackStatus() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
return AttackStatus.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Fallback getFallback() {
|
||||
// We only have one single instance of Fallback,
|
||||
// and we don't want anyone else to create a new instance.
|
||||
// We don't want anyone else to create a new instance.
|
||||
return Fallback.INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ public final class SonarVersion {
|
||||
static final SonarVersion GET = new SonarVersion();
|
||||
private final String semanticVersion, full, formatted, commitSHA;
|
||||
private final int build;
|
||||
private final boolean isOnMainBranch;
|
||||
|
||||
SonarVersion() {
|
||||
final Package manifest = Sonar.class.getPackage();
|
||||
@@ -31,11 +32,12 @@ public final class SonarVersion {
|
||||
|
||||
if (versionString == null) {
|
||||
this.full = "<could not retrieve version>";
|
||||
this.semanticVersion = "<unknown>";
|
||||
this.commitSHA = "<unknown>";
|
||||
this.build = 0;
|
||||
this.semanticVersion = "<unknown version>";
|
||||
this.commitSHA = "<unknown commit>";
|
||||
this.build = -1;
|
||||
this.isOnMainBranch = false;
|
||||
this.formatted = full;
|
||||
Sonar.get().getLogger().error("Could not find version information (Is the manifest missing?)");
|
||||
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ public final class SonarVersion {
|
||||
this.semanticVersion = versionString.split("-")[0];
|
||||
this.commitSHA = versionString.split("-")[1];
|
||||
this.build = Integer.parseInt(versionString.split("-")[2]);
|
||||
this.isOnMainBranch = versionString.split("-")[3].equals("main");
|
||||
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.attack;
|
||||
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackStatus implements JVMProfiler {
|
||||
public static final AttackStatus INSTANCE = new AttackStatus();
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getJoinsPerSecond().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();
|
||||
|
||||
if (joinsPerSecond > minPlayers // check the number of bots/joins per second
|
||||
|| verifyingPlayers > minPlayers // check the number of verifying players
|
||||
|| queuedPlayers > minPlayers) { // check the number of queued players
|
||||
// Increment attack threshold
|
||||
++attackThreshold;
|
||||
|
||||
// An attack has been detected
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
}
|
||||
|
||||
// Update attack statistics
|
||||
if (joinsPerSecond > currentAttack.peakJoinsPerSecond) {
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final double processCPUUsage = getProcessCPUUsage();
|
||||
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
|
||||
currentAttack.peakProcessCPUUsage = processCPUUsage;
|
||||
}
|
||||
final long processMemoryUsage = getUsedMemory();
|
||||
if (processMemoryUsage > currentAttack.peakProcessMemoryUsage) {
|
||||
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
|
||||
}
|
||||
} else if (currentAttack != null) {
|
||||
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
|
||||
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
|
||||
// An attack has stopped
|
||||
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
|
||||
|
||||
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
|
||||
// Save attack statistics
|
||||
final long deltaInMillis = currentAttack.duration.delay();
|
||||
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
|
||||
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
|
||||
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
|
||||
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
|
||||
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%",
|
||||
Sonar.DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%total-success%",
|
||||
Sonar.DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset the attack status
|
||||
currentAttack = null;
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
} else {
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,14 @@ package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSender sender;
|
||||
private final Subcommand command;
|
||||
private final InvocationSource sender;
|
||||
private final Subcommand subcommand;
|
||||
private final String[] rawArguments;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class InvocationSource {
|
||||
private final String name;
|
||||
private final Audience audience;
|
||||
private final boolean player;
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
*/
|
||||
public final void sendMessage() {
|
||||
sendMessage(Component.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*/
|
||||
public final void sendMessage(final String legacy) {
|
||||
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*/
|
||||
public final void sendMessage(final Component component) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
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.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public interface SonarCommand {
|
||||
List<String> TAB_SUGGESTIONS = new ArrayList<>();
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,16 @@ package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -32,10 +36,8 @@ import java.util.stream.Collectors;
|
||||
public abstract class Subcommand {
|
||||
private final @NotNull SubcommandInfo info;
|
||||
private final String permission, aliases, arguments;
|
||||
|
||||
protected static final Sonar SONAR = Sonar.get();
|
||||
protected static final Pattern IP_PATTERN = Pattern.compile(
|
||||
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
);
|
||||
|
||||
public Subcommand() {
|
||||
info = getClass().getAnnotation(SubcommandInfo.class);
|
||||
@@ -49,12 +51,63 @@ public abstract class Subcommand {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
protected final void incorrectUsage(final @NotNull InvocationSender sender) {
|
||||
sender.sendMessage(
|
||||
SONAR.getConfig().INCORRECT_COMMAND_USAGE
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")")
|
||||
);
|
||||
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
// We perform a regex check beforehand,
|
||||
// so we don't have to create a new InetAddress,
|
||||
// therefore, potentially saving some performance.
|
||||
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
}
|
||||
|
||||
public abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
|
||||
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")"));
|
||||
}
|
||||
|
||||
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
|
||||
// Check if the subcommand can only be executed by players
|
||||
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the subcommand can only be executed though console
|
||||
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command
|
||||
execute(commandInvocation);
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
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.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -27,15 +28,19 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
|
||||
@Getter
|
||||
public final class SimpleYamlConfig {
|
||||
private final File file;
|
||||
private final YamlFile yaml;
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
"Sonar anti-bot version " + Sonar.get().getVersion(),
|
||||
" Currently running on: " + Sonar.get().getServer().getPlatform().getDisplayName(),
|
||||
" Need help? https://jonesdev.xyz/discord",
|
||||
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",
|
||||
""
|
||||
);
|
||||
|
||||
@@ -43,11 +48,12 @@ public final class SimpleYamlConfig {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final File folder) {
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
|
||||
this.file = file;
|
||||
this.yaml = new YamlFile(file.getPath());
|
||||
}
|
||||
|
||||
@@ -59,16 +65,18 @@ public final class SimpleYamlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
try {
|
||||
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
|
||||
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();
|
||||
|
||||
yaml.options().headerFormatter().commentPrefix("# ");
|
||||
yaml.setHeader(String.join(Sonar.LINE_SEPARATOR, HEADER));
|
||||
} catch (IOException exception) {
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
|
||||
// Always load the comment format and options
|
||||
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
|
||||
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
|
||||
}
|
||||
|
||||
public void set(final String path, final Object v) {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+20
-10
@@ -45,16 +45,14 @@ public final class VerifiedPlayerController {
|
||||
private Dao<VerifiedPlayer, Integer> dao;
|
||||
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
|
||||
@Getter
|
||||
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
|
||||
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
|
||||
|
||||
public VerifiedPlayerController() {
|
||||
final SonarConfiguration config = Sonar.get().getConfig();
|
||||
|
||||
// Cache selected database type, so we don't need to call Sonar.get() every time
|
||||
cachedDatabaseType = config.DATABASE_TYPE;
|
||||
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
|
||||
|
||||
// Don't establish a database connection if the type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
|
||||
return;
|
||||
}
|
||||
@@ -67,6 +65,8 @@ public final class VerifiedPlayerController {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException exception) {
|
||||
// Duplicate index
|
||||
// I know this isn't the best method of handling it,
|
||||
// but I don't know how else I could address this issue.
|
||||
}
|
||||
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
@@ -89,12 +89,17 @@ public final class VerifiedPlayerController {
|
||||
_remove(inetAddress);
|
||||
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// 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) {
|
||||
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
|
||||
@@ -131,12 +136,17 @@ public final class VerifiedPlayerController {
|
||||
_add(player);
|
||||
|
||||
// Don't try to update the column if the database type is NONE
|
||||
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
|
||||
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// 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) {
|
||||
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dao.create(player);
|
||||
@@ -181,7 +191,7 @@ public final class VerifiedPlayerController {
|
||||
MAP.clear();
|
||||
|
||||
// Only update the column if the database type is not NONE
|
||||
if (cachedDatabaseType != SonarConfiguration.DatabaseType.NONE) {
|
||||
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
|
||||
dao.deleteBuilder().delete();
|
||||
}
|
||||
} catch (SQLException exception) {
|
||||
|
||||
@@ -28,7 +28,8 @@ import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
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
|
||||
@@ -37,8 +38,9 @@ public enum Dependency {
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.1.0"
|
||||
"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",
|
||||
@@ -46,10 +48,8 @@ public enum Dependency {
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private @NotNull
|
||||
final Path tempFilePath;
|
||||
private @NotNull
|
||||
final URL mvnRepoURL;
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
@@ -77,13 +77,12 @@ public enum Dependency {
|
||||
|
||||
try (final InputStream inputStream = mvnRepoURL.openStream()) {
|
||||
Files.createDirectories(tempFilePath.getParent());
|
||||
Files.copy(inputStream, Files.createFile(tempFilePath), StandardCopyOption.REPLACE_EXISTING);
|
||||
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.formattedDelay());
|
||||
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
|
||||
}
|
||||
|
||||
return tempFilePath.toUri().toURL();
|
||||
|
||||
@@ -38,24 +38,19 @@ public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration config = Sonar.get().getConfig();
|
||||
|
||||
final URL[] urls = new URL[config.DATABASE_TYPE.getDependencies().length];
|
||||
for (final Dependency dependency : config.DATABASE_TYPE.getDependencies()) {
|
||||
final URL url = dependency.getClassLoaderURL();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
final URL url = config.getDatabase().getType().getDependency().getClassLoaderURL();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
urls[dependency.ordinal()] = url;
|
||||
}
|
||||
final String type = config.getDatabase().getType().name().toLowerCase();
|
||||
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
|
||||
|
||||
final String type = config.DATABASE_TYPE.name().toLowerCase();
|
||||
final String databaseURL =
|
||||
"jdbc:" + type + "://" + config.SQL_URL + ":" + config.SQL_PORT + "/" + config.SQL_DATABASE;
|
||||
|
||||
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
|
||||
final Connection connection = connect(classLoader, databaseURL, config);
|
||||
final ExternalClassLoader classLoader = new ExternalClassLoader(new URL[] {url});
|
||||
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
|
||||
return new JdbcSingleConnectionSource(databaseURL, connection);
|
||||
}
|
||||
|
||||
@@ -63,18 +58,16 @@ public class DependencyLoader {
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration configuration) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(configuration.DATABASE_TYPE.getDriverClassName());
|
||||
final @NotNull SonarConfiguration.Database config) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
if (!configuration.SQL_USER.isEmpty()) {
|
||||
properties.put("user", configuration.SQL_USER);
|
||||
}
|
||||
if (!configuration.SQL_PASSWORD.isEmpty()) {
|
||||
properties.put("password", configuration.SQL_PASSWORD);
|
||||
properties.put("user", config.getUsername());
|
||||
if (!config.getPassword().isEmpty()) {
|
||||
properties.put("password", config.getPassword());
|
||||
}
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event;
|
||||
|
||||
public interface SonarEvent {
|
||||
}
|
||||
+6
-17
@@ -15,26 +15,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
package xyz.jonesdev.sonar.api.event;
|
||||
|
||||
public interface InvocationSender {
|
||||
@FunctionalInterface
|
||||
public interface SonarEventListener {
|
||||
|
||||
/**
|
||||
* @return The name of the command executor
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
* Handles the publishing of an event
|
||||
*
|
||||
* @param message Deserialized message
|
||||
* @param event [Cancellable] Sonar event
|
||||
*/
|
||||
void sendMessage(final String message);
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
*/
|
||||
default void sendMessage() {
|
||||
sendMessage("");
|
||||
}
|
||||
void handle(final SonarEvent event);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Vector;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class SonarEventManager {
|
||||
public static final SonarEventManager INSTANCE = new SonarEventManager();
|
||||
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
|
||||
|
||||
/**
|
||||
* Publishes the given event to all listeners
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void publish(final @NotNull SonarEvent event) {
|
||||
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
|
||||
eventListener.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers one (or more) event listeners
|
||||
*
|
||||
* @param listeners One (or more) listeners to register
|
||||
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
|
||||
*/
|
||||
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters one (or more) event listeners
|
||||
*
|
||||
* @param listeners One (or more) listeners to unregister
|
||||
* @apiNote This does not have any effect if the given listeners are not registered
|
||||
* @see #registerListener(SonarEventListener...) Register an event listener
|
||||
*/
|
||||
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
|
||||
}
|
||||
}
|
||||
+3
-11
@@ -15,17 +15,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public interface SonarBaseCommand {
|
||||
List<String> TAB_SUGGESTIONS = new ArrayList<>();
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
List<Object> CACHED_HELP_MESSAGE = new Vector<>();
|
||||
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
|
||||
public final class AttackDetectedEvent implements SonarEvent {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackStatus;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackStatus.AttackStatistics attackStatistics;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserBlacklistedEvent implements SonarEvent {
|
||||
@ApiStatus.Experimental
|
||||
private final FallbackUser<?, ?> user;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser<?, ?> user;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyJoinEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final FallbackUser<?, ?> user;
|
||||
}
|
||||
+16
-19
@@ -15,27 +15,24 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.verbose;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final String message) {
|
||||
final Component component = Component.text(message);
|
||||
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
server.getPlayer(subscriber).ifPresent(player -> {
|
||||
player.sendActionBar(component);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
public final class UserVerifySuccessEvent implements SonarEvent {
|
||||
private final String username;
|
||||
private final UUID offlineUuid;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final long timeTakenToVerify;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public final class Fallback {
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 2500L
|
||||
10L, TimeUnit.MINUTES, 5000L
|
||||
);
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
@@ -50,22 +50,17 @@ public final class Fallback {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
SONAR.getLogger().info("[Fallback] " + message, args);
|
||||
SONAR.getLogger().info("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
SONAR.getLogger().warn("[Fallback] " + message, args);
|
||||
SONAR.getLogger().warn("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
SONAR.getLogger().error("[Fallback] " + message, args);
|
||||
SONAR.getLogger().error("[fallback] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isPotentiallyUnderAttack() {
|
||||
return connected.size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK
|
||||
|| queue.getQueuedPlayers().size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK;
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,14 @@ public final class FallbackQueue {
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
final int max = Sonar.get().getConfig().MAXIMUM_QUEUE_POLLS;
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// We need to be very careful here since we don't want any concurrency issues.
|
||||
// We need to be cautious here since we don't want any concurrency issues.
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > max) {
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
// Run the cached runnable
|
||||
@@ -66,5 +66,8 @@ public final class FallbackQueue {
|
||||
// Remove runnable from iterator
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackStatus().checkIfUnderAttack();
|
||||
}
|
||||
}
|
||||
+32
-6
@@ -19,18 +19,21 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface FallbackPlayer<X, Y> {
|
||||
public interface FallbackUser<X, Y> {
|
||||
@NotNull Fallback getFallback();
|
||||
|
||||
@NotNull X getPlayer();
|
||||
@@ -49,9 +52,20 @@ public interface FallbackPlayer<X, Y> {
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
*
|
||||
* @param reason Disconnect message
|
||||
* @param reason Legacy disconnect message string
|
||||
* @see #disconnect(Component)
|
||||
*/
|
||||
void disconnect(final @NotNull String reason);
|
||||
default void disconnect(final @NotNull String reason) {
|
||||
disconnect(Component.text(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks the player from the server with
|
||||
* the given disconnect message.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
@@ -60,6 +74,12 @@ public interface FallbackPlayer<X, Y> {
|
||||
*/
|
||||
void write(final @NotNull Object msg);
|
||||
|
||||
/**
|
||||
* Queues a buffered message that will be
|
||||
* sent once all messages are flushed.
|
||||
*/
|
||||
void delayedWrite(final @NotNull Object msg);
|
||||
|
||||
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
|
||||
|
||||
/**
|
||||
@@ -72,10 +92,10 @@ public interface FallbackPlayer<X, Y> {
|
||||
*/
|
||||
default void fail(final @Nullable String reason) {
|
||||
if (getChannel().isActive()) {
|
||||
disconnect(Sonar.get().getConfig().VERIFICATION_FAILED);
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_FAILED_LOG
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
@@ -84,13 +104,19 @@ public interface FallbackPlayer<X, Y> {
|
||||
|
||||
Statistics.FAILED_VERIFICATIONS.increment();
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
|
||||
// Check if the player has too many failed attempts
|
||||
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress().toString());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_BLACKLIST_LOG
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
} else {
|
||||
+7
-12
@@ -17,12 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum ProtocolVersion {
|
||||
@@ -66,12 +64,13 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_3(761),
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(-1);
|
||||
MINECRAFT_1_20_2(764);
|
||||
|
||||
@Getter
|
||||
private final int protocol;
|
||||
private final int snapshotProtocol = -1;
|
||||
|
||||
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
|
||||
public static final ProtocolVersion LATEST_VERSION;
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
|
||||
|
||||
@@ -81,7 +80,6 @@ public enum ProtocolVersion {
|
||||
|
||||
for (final ProtocolVersion version : values()) {
|
||||
versions.putIfAbsent(version.protocol, version);
|
||||
versions.put(version.snapshotProtocol, version);
|
||||
}
|
||||
|
||||
ID_TO_PROTOCOL_CONSTANT = versions;
|
||||
@@ -96,7 +94,8 @@ public enum ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
SUPPORTED_VERSIONS = versions;
|
||||
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
|
||||
LATEST_VERSION = new ArrayList<>(versions).get(versions.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +103,6 @@ public enum ProtocolVersion {
|
||||
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol == -1 ? snapshotProtocol : protocol;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return this == UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.logger;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
|
||||
/**
|
||||
* The whole purpose of doing this is, so we can have one logger for every module.
|
||||
* We wouldn't need to get the plugin logger every time we try to use it.
|
||||
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
|
||||
void warn(final String message, final Object... args);
|
||||
|
||||
void error(final String message, final Object... args);
|
||||
|
||||
// Taken from
|
||||
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
|
||||
String ARG_STRING = "{}";
|
||||
int ARG_STRING_LENGTH = ARG_STRING.length();
|
||||
Object UNKNOWN_ARG = new Object();
|
||||
|
||||
default String buildFullMessage(final @NotNull String msg, final Object... args) {
|
||||
StringBuilder sb = null;
|
||||
int lastIndex = 0;
|
||||
int argC = 0;
|
||||
while (true) {
|
||||
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
|
||||
// no more {} arguments?
|
||||
if (argIndex == -1) {
|
||||
break;
|
||||
}
|
||||
if (sb == null) {
|
||||
// we build this lazily in case there is no {} in the msg
|
||||
sb = new StringBuilder(128);
|
||||
}
|
||||
// add the string before the arg-string
|
||||
sb.append(msg, lastIndex, argIndex);
|
||||
// shift our last-index past the arg-string
|
||||
lastIndex = argIndex + ARG_STRING_LENGTH;
|
||||
// add the arguments
|
||||
if (argC < args.length) {
|
||||
appendArg(sb, args[argC]);
|
||||
}
|
||||
argC++;
|
||||
}
|
||||
if (sb == null) {
|
||||
return msg;
|
||||
} else {
|
||||
sb.append(msg, lastIndex, msg.length());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
|
||||
if (arg == UNKNOWN_ARG) {
|
||||
// ignore it
|
||||
} else if (arg == null) {
|
||||
stringBuilder.append("null");
|
||||
} else if (arg.getClass().isArray()) {
|
||||
stringBuilder.append('[');
|
||||
int length = Array.getLength(arg);
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i > 0) {
|
||||
stringBuilder.append(", ");
|
||||
}
|
||||
appendArg(stringBuilder, Array.get(arg, i));
|
||||
}
|
||||
stringBuilder.append(']');
|
||||
} else {
|
||||
stringBuilder.append(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ public final class SystemTimer {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
public String formattedDelay() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
}
|
||||
}
|
||||
@@ -19,53 +19,60 @@ package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import 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;
|
||||
|
||||
@Getter
|
||||
public abstract class Verbose implements JVMProfiler {
|
||||
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final SystemTimer secondTimer = new SystemTimer();
|
||||
protected int joinsPerSecond, totalJoins;
|
||||
private int lastTotalJoins, animationIndex;
|
||||
public final class Verbose implements JVMProfiler {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> joinsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public final void update() {
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
joinsPerSecond.cleanUp(false);
|
||||
|
||||
totalJoins = Statistics.TOTAL_TRAFFIC.get();
|
||||
|
||||
// Statistically determine the joins per second without any caches
|
||||
if (totalJoins > 0 && secondTimer.delay() >= 1000L) {
|
||||
secondTimer.reset();
|
||||
joinsPerSecond = totalJoins - lastTotalJoins;
|
||||
lastTotalJoins = totalJoins;
|
||||
}
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
broadcast(prepareActionBarFormat());
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = audiences.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
}
|
||||
|
||||
protected final @NotNull String prepareActionBarFormat() {
|
||||
return Sonar.get().getConfig().ACTION_BAR_LAYOUT
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
|
||||
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
@@ -78,37 +85,34 @@ public abstract class Verbose implements JVMProfiler {
|
||||
.replace("%free-memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%total-memory%", formatMemory(getTotalMemory()))
|
||||
.replace("%max-memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%animation%", nextAnimation());
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
protected final String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().ANIMATION;
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
// Run action bar verbose
|
||||
protected abstract void broadcast(final String message);
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player who subscribed
|
||||
* @return Whether the player is subscribed or not
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public boolean isSubscribed(final @NotNull String subscriber) {
|
||||
return subscribers.contains(subscriber);
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to subscribe
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public void subscribe(final @NotNull String subscriber) {
|
||||
subscribers.add(subscriber);
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to unsubscribe
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public void unsubscribe(final @NotNull String subscriber) {
|
||||
subscribers.remove(subscriber);
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.webhook;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class DiscordWebhook {
|
||||
private final String url;
|
||||
|
||||
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
|
||||
HTTP_REQUEST_SERVICE.execute(() -> {
|
||||
try {
|
||||
final String serializedContent = prepareSerializedContent(embed.get());
|
||||
final HttpsURLConnection urlConnection = prepareConnection(new URL(url), serializedContent);
|
||||
|
||||
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
// Check if the connection was a success
|
||||
if (urlConnection.getResponseCode() != 204) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
final String content = Sonar.get().getConfig().getWebhook().getContent();
|
||||
final String username = Sonar.get().getConfig().getWebhook().getUsername();
|
||||
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
|
||||
|
||||
int rgb = embed.getR();
|
||||
rgb = (rgb << 8) + embed.getG();
|
||||
rgb = (rgb << 8) + embed.getB();
|
||||
|
||||
final Webhook.EmbedMessage embedMessage = new Webhook.EmbedMessage(
|
||||
embed.getTitle(), embed.getDescription(), embed.getTitleUrl(), rgb, new Webhook.Field[0],
|
||||
new Webhook.Footer(Sonar.get().getConfig().getWebhook().getFooter().getText(),
|
||||
Sonar.get().getConfig().getWebhook().getFooter().getIconUrl()));
|
||||
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
|
||||
|
||||
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
|
||||
return Sonar.GENERAL_GSON.toJson(webhook);
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
|
||||
final @NotNull String serializedContent) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setRequestProperty("Content-Length", String.valueOf(serializedContent.length()));
|
||||
urlConnection.setRequestProperty("User-Agent", "Sonar");
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setRequestMethod("POST");
|
||||
return urlConnection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
static final class Webhook {
|
||||
private final String content, username, avatar_url;
|
||||
private final boolean tts;
|
||||
private final List<EmbedMessage> embeds;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class EmbedMessage {
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final String url;
|
||||
private final int color;
|
||||
private final Field[] fields;
|
||||
private final Footer footer;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Field {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final boolean inline;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Footer {
|
||||
private final String text;
|
||||
private final String icon_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,16 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString()
|
||||
name = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
permission = "sonar.command"
|
||||
// No permission check here
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +30,9 @@ dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
|
||||
|
||||
@@ -18,30 +18,32 @@
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
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.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for Bukkit audiences
|
||||
*/
|
||||
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -52,64 +54,31 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper() {
|
||||
|
||||
@Override
|
||||
public SonarPlatform getPlatform() {
|
||||
return SonarPlatform.VELOCITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSender> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getOnlinePlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(player -> new InvocationSender() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return player.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final String message) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
});
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// Reload configuration
|
||||
reload();
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new SonarCommand());
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
|
||||
@@ -123,9 +92,4 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
|
||||
2L, 2L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postReload() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+8
-20
@@ -15,28 +15,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.verbose;
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class ActionBarVerbose extends Verbose {
|
||||
private final Server server;
|
||||
|
||||
public void broadcast(final String message) {
|
||||
final TextComponent component = new TextComponent(message);
|
||||
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final Player player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
//player.sendMessage(ChatMessageType.ACTION_BAR, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
public final class BukkitInvocationSource extends InvocationSource {
|
||||
public BukkitInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender), sender instanceof Player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(final CommandSender sender,
|
||||
final Command command,
|
||||
final String label,
|
||||
final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
|
||||
|
||||
if (invocationSource.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return false;
|
||||
}
|
||||
// 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(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return false;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
} else {
|
||||
subcommand.get().invoke(invocationSource, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(final @NotNull CommandSender sender,
|
||||
final Command command,
|
||||
final String commandAlias,
|
||||
final String @NotNull [] args) {
|
||||
// Do not allow tab completion if the player does not have the required permission
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBaseCommand {
|
||||
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
|
||||
".com/jonesdevelopment/sonar");
|
||||
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
|
||||
"Discord ");
|
||||
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
|
||||
"GitHub.");
|
||||
|
||||
static {
|
||||
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
|
||||
".com/jonesdevelopment/sonar"));
|
||||
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
|
||||
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
|
||||
"Discord)").create()));
|
||||
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
|
||||
"/issues"));
|
||||
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
|
||||
"GitHub)").create()));
|
||||
}
|
||||
|
||||
static {
|
||||
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
|
||||
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
|
||||
+ "."),
|
||||
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
|
||||
GITHUB_LINK_COMPONENT,
|
||||
new TextComponent(""),
|
||||
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
|
||||
new TextComponent(
|
||||
DISCORD_SUPPORT,
|
||||
GITHUB_ISSUES
|
||||
),
|
||||
new TextComponent("")
|
||||
));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
|
||||
final TextComponent component = new TextComponent(
|
||||
new TextComponent(ChatColor.GRAY + " ▪ "),
|
||||
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
|
||||
new TextComponent(ChatColor.GRAY + " - "),
|
||||
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
|
||||
);
|
||||
|
||||
component.setClickEvent(
|
||||
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
|
||||
);
|
||||
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
|
||||
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
|
||||
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
|
||||
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
|
||||
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
|
||||
).create())
|
||||
);
|
||||
CACHED_HELP_MESSAGE.add(component);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(final CommandSender sender,
|
||||
final Command command,
|
||||
final String label,
|
||||
final String[] args) {
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// 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(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
sender.sendMessage(Sonar.get().getConfig().COMMAND_COOL_DOWN);
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
sender.sendMessage(
|
||||
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
final InvocationSender invocationSender = new InvocationSender() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return sender.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final String message) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSender.sendMessage(
|
||||
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
|
||||
.replace("%permission%", subcommand.get().getPermission())
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
|
||||
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (sub.getInfo().arguments().length > 0
|
||||
&& commandInvocation.getRawArguments().length <= 1) {
|
||||
invocationSender.sendMessage(
|
||||
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Object component : CACHED_HELP_MESSAGE) {
|
||||
if (sender instanceof Player) {
|
||||
((Player) sender).spigot().sendMessage((TextComponent) component);
|
||||
} else {
|
||||
sender.sendMessage(((TextComponent) component).getText());
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(final CommandSender sender,
|
||||
final Command command,
|
||||
final String commandAlias,
|
||||
final String @NotNull [] args) {
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
@@ -5,25 +5,21 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
name = rootProject.name
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
name = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
|
||||
implementation("com.velocitypowered:velocity-native:1.1.9") {
|
||||
exclude(group = "com.google.guava")
|
||||
exclude(group = "io.netty")
|
||||
exclude(group = "org.checkerframework")
|
||||
}
|
||||
|
||||
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
}
|
||||
|
||||
@@ -18,34 +18,35 @@
|
||||
package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.command.SonarCommand;
|
||||
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.verbose.VerboseWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for BungeeCord audiences
|
||||
*/
|
||||
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -56,67 +57,34 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper() {
|
||||
|
||||
@Override
|
||||
public SonarPlatform getPlatform() {
|
||||
return SonarPlatform.BUNGEE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSender> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(player -> new InvocationSender() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return player.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final String message) {
|
||||
player.sendMessage(new TextComponent(message));
|
||||
}
|
||||
});
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// Reload configuration
|
||||
reload();
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new SonarCommand());
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
|
||||
@@ -133,11 +101,4 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postReload() {
|
||||
|
||||
// Prepare cached messages
|
||||
FallbackListener.CachedMessages.update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class BungeeInvocationSource extends InvocationSource {
|
||||
public BungeeInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), SonarBungee.INSTANCE.getBungeeAudiences().sender(sender), sender instanceof ProxiedPlayer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
|
||||
public BungeeSonarCommand() {
|
||||
super("sonar");
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"redundantSuppression"})
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
|
||||
if (invocationSource.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.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(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
} else {
|
||||
subcommand.get().invoke(invocationSource, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
|
||||
// Do not allow tab completion if the player does not have the required permission
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import net.md_5.bungee.command.ConsoleCommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class SonarCommand extends Command implements TabExecutor, SonarBaseCommand {
|
||||
public SonarCommand() {
|
||||
super("sonar", "sonar.command");
|
||||
}
|
||||
|
||||
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
|
||||
".com/jonesdevelopment/sonar");
|
||||
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
|
||||
"Discord ");
|
||||
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
|
||||
"GitHub.");
|
||||
|
||||
static {
|
||||
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
|
||||
".com/jonesdevelopment/sonar"));
|
||||
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
|
||||
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
|
||||
"Discord)").create()));
|
||||
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
|
||||
"/issues"));
|
||||
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
|
||||
"GitHub)").create()));
|
||||
}
|
||||
|
||||
static {
|
||||
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
|
||||
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
|
||||
+ "."),
|
||||
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
|
||||
GITHUB_LINK_COMPONENT,
|
||||
new TextComponent(""),
|
||||
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
|
||||
new TextComponent(
|
||||
DISCORD_SUPPORT,
|
||||
GITHUB_ISSUES
|
||||
),
|
||||
new TextComponent("")
|
||||
));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
|
||||
final TextComponent component = new TextComponent(
|
||||
new TextComponent(ChatColor.GRAY + " ▪ "),
|
||||
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
|
||||
new TextComponent(ChatColor.GRAY + " - "),
|
||||
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
|
||||
);
|
||||
|
||||
component.setClickEvent(
|
||||
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
|
||||
);
|
||||
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
|
||||
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
|
||||
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
|
||||
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
|
||||
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
|
||||
).create())
|
||||
);
|
||||
CACHED_HELP_MESSAGE.add(component);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"redundantSuppression"})
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// 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(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
sender.sendMessage(new TextComponent(Sonar.get().getConfig().COMMAND_COOL_DOWN));
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
sender.sendMessage(new TextComponent(
|
||||
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
final InvocationSender invocationSender = new InvocationSender() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return sender.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final String message) {
|
||||
sender.sendMessage(new TextComponent(message));
|
||||
}
|
||||
};
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSender.sendMessage(
|
||||
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
|
||||
.replace("%permission%", subcommand.get().getPermission())
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
|
||||
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (sub.getInfo().arguments().length > 0
|
||||
&& commandInvocation.getRawArguments().length <= 1) {
|
||||
invocationSender.sendMessage(
|
||||
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Object component : CACHED_HELP_MESSAGE) {
|
||||
sender.sendMessage((TextComponent) component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
|
||||
final class ConditionFailedException extends DecoderException {
|
||||
public ConditionFailedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import 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 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);
|
||||
handler.connected(channel);
|
||||
|
||||
if (handler instanceof DownstreamBridge) {
|
||||
ProxyServer.getInstance().getLogger().info(handler + " has connected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
handler.disconnected(channel);
|
||||
|
||||
if (handler instanceof DownstreamBridge) {
|
||||
ProxyServer.getInstance().getLogger().info(handler + " has disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.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.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private @NotNull final BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper player;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new ConditionFailedException("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 ConditionFailedException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || player != null) {
|
||||
throw new ConditionFailedException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
try {
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getConfig().getVerification().isEnabled()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
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 the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
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;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
player = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& player.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 (player.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;
|
||||
}
|
||||
|
||||
// We cannot allow too many players on our Fallback server
|
||||
// There's technically no reason for limiting this, but we'll better stay safe.
|
||||
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
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()));
|
||||
|
||||
// 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
|
||||
pipeline.replace(
|
||||
TIMEOUT_HANDLER,
|
||||
TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (player.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().getAttackStatus().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(player.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), player));
|
||||
|
||||
// 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 (player.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| player.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(player.getProtocolVersion());
|
||||
player.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
player.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
player.getPipeline().replace(
|
||||
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,
|
||||
new FallbackVerificationHandler(player, loginRequest.getData(), uuid)
|
||||
));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
|
||||
|
||||
private 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(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
-83
@@ -17,110 +17,87 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
import net.md_5.bungee.compress.PacketDecompressor;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
|
||||
import static xyz.jonesdev.sonar.bungee.fallback.FallbackListener.CachedMessages.LOCKDOWN_DISCONNECT;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
private final Fallback fallback;
|
||||
|
||||
private static final Field CHANNEL_WRAPPER;
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_WRAPPER.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
|
||||
public static class CachedMessages {
|
||||
static TextComponent LOCKDOWN_DISCONNECT;
|
||||
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 */;
|
||||
|
||||
public static void update() {
|
||||
LOCKDOWN_DISCONNECT = new TextComponent(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
|
||||
// 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();
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
fallbackInitialHandler.closeWith(new Kick(serialized));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
|
||||
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
|
||||
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
|
||||
event.getPlayer().disconnect(LOCKDOWN_DISCONNECT);
|
||||
|
||||
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
|
||||
Sonar.get().getLogger().info(
|
||||
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
|
||||
.replace("%player%", event.getPlayer().getName())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion()))
|
||||
);
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
|
||||
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
|
||||
// Try to close the channel with a custom serialized disconnect component
|
||||
if (pendingConnection instanceof FallbackInitialHandler) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
|
||||
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
fallbackInitialHandler.closeWith(new Kick(serialized));
|
||||
} else {
|
||||
// Fallback by disconnecting without a message
|
||||
pendingConnection.disconnect();
|
||||
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getName())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
|
||||
}
|
||||
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
final Audience audience = Sonar.get().getVerboseHandler().getAudiences().get(event.getPlayer().getName());
|
||||
if (audience != null) {
|
||||
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
|
||||
audience.sendMessage(deserialized);
|
||||
}
|
||||
return;
|
||||
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
|
||||
event.getPlayer().sendMessage(
|
||||
new TextComponent(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final InitialHandler initialHandler = (InitialHandler) event.getPlayer().getPendingConnection();
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER.get(initialHandler);
|
||||
|
||||
final ChannelPipeline pipeline = channelWrapper.getHandle().pipeline();
|
||||
|
||||
// You don't need the frame decoder anymore
|
||||
if (pipeline.get(FRAME_PREPENDER) != null) {
|
||||
pipeline.remove(FRAME_PREPENDER);
|
||||
}
|
||||
|
||||
final int compressionThreshold = BungeeCord.getInstance().getConfig().getCompressionThreshold();
|
||||
|
||||
// Replace compression handlers
|
||||
if (compressionThreshold > 0
|
||||
&& pipeline.get(PacketCompressor.class) != null
|
||||
&& pipeline.get(PacketDecompressor.class) != null) {
|
||||
final VelocityCompressor compressor = Natives.compress.get().create(-1);
|
||||
|
||||
// Replace (de)compressor with Velocity's to ensure better performance
|
||||
pipeline.replace(
|
||||
PacketCompressor.class,
|
||||
"compress",
|
||||
// Create a new compressor instance with the Velocity compressor
|
||||
new FallbackPacketCompressor(compressionThreshold, compressor)
|
||||
);
|
||||
pipeline.replace(
|
||||
PacketDecompressor.class,
|
||||
"decompress",
|
||||
// Create a new decompressor instance with the Velocity decompressor
|
||||
new FallbackPacketDecompressor(compressionThreshold, compressor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -22,20 +22,19 @@ import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrapper, FallbackInitialHandler> {
|
||||
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
|
||||
private final Fallback fallback;
|
||||
private final ChannelWrapper player;
|
||||
private final FallbackInitialHandler connection;
|
||||
@@ -45,8 +44,8 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
|
||||
private final ProtocolVersion protocolVersion;
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull String reason) {
|
||||
connection.closeWith(Disconnect.create(TextComponent.toLegacyText(new TextComponent(reason))));
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,4 +56,13 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delayedWrite(final @NotNull Object packet) {
|
||||
if (channel.isActive()) {
|
||||
channel.write(packet, channel.voidPromise());
|
||||
} else {
|
||||
ReferenceCountUtil.release(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.zip.DataFormatException;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public final class FallbackPacketCompressor extends PacketCompressor {
|
||||
private int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
|
||||
|
||||
@Override
|
||||
public void setThreshold(final int compressionThreshold) {
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws Exception {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
// Under the threshold, do nothing.
|
||||
writeVarInt(out, uncompressed + 1);
|
||||
writeVarInt(out, 0);
|
||||
out.writeBytes(msg);
|
||||
} else {
|
||||
handleCompressed(ctx, msg, out);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws DataFormatException {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
|
||||
write21BitVarInt(out, 0); // Fake packet length
|
||||
writeVarInt(out, uncompressed);
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
|
||||
|
||||
final int startCompressed = out.writerIndex();
|
||||
try {
|
||||
compressor.deflate(compatibleIn, out);
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
|
||||
final int compressedLength = out.writerIndex() - startCompressed;
|
||||
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
|
||||
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
|
||||
}
|
||||
|
||||
int writerIndex = out.writerIndex();
|
||||
int packetLength = out.readableBytes() - 3;
|
||||
out.writerIndex(0);
|
||||
write21BitVarInt(out, packetLength); // Rewrite packet length
|
||||
out.writerIndex(writerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final boolean preferDirect) throws Exception {
|
||||
int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
int finalBufferSize = uncompressed + 1;
|
||||
finalBufferSize += varIntBytes(finalBufferSize);
|
||||
return ctx.alloc().directBuffer(finalBufferSize);
|
||||
}
|
||||
|
||||
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
|
||||
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketDecompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
public final class FallbackPacketDecompressor extends PacketDecompressor {
|
||||
private final int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
|
||||
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
|
||||
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
|
||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||
|
||||
public FallbackPacketDecompressor(final int compressionThreshold,
|
||||
final @NotNull VelocityCompressor compressor) {
|
||||
super(); // super(compressionThreshold);
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
this.compressor = compressor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf in,
|
||||
final List<Object> out) throws Exception {
|
||||
final int claimedUncompressedSize = readVarInt(in);
|
||||
if (claimedUncompressedSize == 0) {
|
||||
// This message is not compressed.
|
||||
out.add(in.retain());
|
||||
|
||||
// Check for invalid packet size after the packet is retained
|
||||
final int size = in.readableBytes();
|
||||
if (size > compressionThreshold + 2) {
|
||||
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (claimedUncompressedSize < compressionThreshold) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
|
||||
}
|
||||
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
|
||||
}
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||
|
||||
try {
|
||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||
out.add(uncompressed);
|
||||
} catch (Exception exception) {
|
||||
uncompressed.release();
|
||||
throw exception;
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.handler;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
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.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackPlayerWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
private ChannelWrapper channelWrapper;
|
||||
@SuppressWarnings("unused") // TODO: remove later
|
||||
private @Nullable FallbackPlayerWrapper player;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
|
||||
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
player = new FallbackPlayerWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
|
||||
);
|
||||
// TODO: implement Fallback
|
||||
}
|
||||
|
||||
super.handle(loginRequest);
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (player != null && player.getChannel().isActive()) {
|
||||
boolean is17 = player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
player.getChannel().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
player.getChannel().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-18
@@ -17,23 +17,19 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
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.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
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.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
@@ -42,18 +38,15 @@ import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
|
||||
|
||||
// cached object of our configuration to instantly access it
|
||||
private static final SonarConfiguration CONFIG = Objects.requireNonNull(Sonar.get().getConfig());
|
||||
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 WriteBufferWaterMark MARK;
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field markField = PipelineUtils.class.getDeclaredField("MARK");
|
||||
markField.setAccessible(true);
|
||||
MARK = (WriteBufferWaterMark) markField.get(null);
|
||||
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
@@ -72,15 +65,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
|
||||
channel.config().setWriteBufferWaterMark(MARK);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
channel.pipeline().addLast(FRAME_DECODER, CONFIG.REPLACE_VAR_INT_DECODER
|
||||
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
|
||||
));
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -23,15 +23,15 @@ 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.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
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.bungee.fallback.handler.FallbackInitialHandler;
|
||||
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;
|
||||
@@ -43,7 +43,7 @@ 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 {
|
||||
@@ -63,7 +63,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
@@ -80,13 +80,12 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
|
||||
ProxyServer.getInstance().getProtocolVersion()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
|
||||
true, ProxyServer.getInstance().getProtocolVersion()));
|
||||
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(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
|
||||
listener));
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ 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;
|
||||
|
||||
@@ -29,7 +30,7 @@ import java.util.List;
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.verbose;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
public void broadcast(final String message) {
|
||||
final TextComponent component = new TextComponent(message);
|
||||
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final ProxiedPlayer player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
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;
|
||||
@@ -32,6 +34,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
@@ -41,35 +44,45 @@ import java.util.concurrent.TimeUnit;
|
||||
@RequiredArgsConstructor
|
||||
public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private T plugin;
|
||||
@Setter
|
||||
private Verbose verboseHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final File dataDirectory,
|
||||
final Verbose verboseHandler) {
|
||||
// Set the API to this instance so the config doesn't have issues
|
||||
final SonarPlatform platform) {
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
|
||||
// Set the plugin instance before anything else
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.verboseHandler = verboseHandler;
|
||||
this.platform = platform;
|
||||
this.verboseHandler = new Verbose();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
}
|
||||
|
||||
public final void initialize() {
|
||||
getLogger().info("Successfully booted in {}s!", launchTimer.formattedDelay());
|
||||
// Check if the branch is not the main branch to warn about unstable versions
|
||||
if (!getVersion().isOnMainBranch()) {
|
||||
getLogger().warn("You are currently using an unofficial experimental branch.");
|
||||
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
|
||||
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
}
|
||||
|
||||
getLogger().info("Successfully booted in {}s!", launchTimer);
|
||||
getLogger().info("Initializing shared components...");
|
||||
|
||||
// Reload configuration
|
||||
reload();
|
||||
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer.formattedDelay());
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer);
|
||||
getLogger().info("Enabling all tasks and features...");
|
||||
|
||||
try {
|
||||
@@ -77,11 +90,16 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
enable();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer.formattedDelay());
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
} catch (Throwable throwable) {
|
||||
// An error has occurred
|
||||
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
|
||||
throwable.printStackTrace(System.err);
|
||||
return; // Do not check for updates if the launch failed
|
||||
}
|
||||
|
||||
// Check if a new version has been released
|
||||
UpdateChecker.checkForUpdates();
|
||||
}
|
||||
|
||||
public abstract void enable();
|
||||
@@ -92,7 +110,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
// Warn player if they reloaded and changed the database type
|
||||
if (getVerifiedPlayerController() != null
|
||||
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().DATABASE_TYPE) {
|
||||
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
|
||||
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
|
||||
+ " is generally not recommended as it can sometimes cause data loss.");
|
||||
}
|
||||
@@ -102,19 +120,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
// Update ratelimiter
|
||||
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
|
||||
getConfig().VERIFICATION_DELAY, TimeUnit.MILLISECONDS, 250L
|
||||
);
|
||||
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
|
||||
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
|
||||
|
||||
// Reinitialize database controller
|
||||
verifiedPlayerController = new VerifiedPlayerController();
|
||||
|
||||
// Call post reload task
|
||||
postReload();
|
||||
}
|
||||
|
||||
public abstract void postReload();
|
||||
|
||||
public void shutdown() {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// ...
|
||||
|
||||
+7
-8
@@ -20,24 +20,23 @@ 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;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final String username;
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final ChannelHandlerContext ctx) {
|
||||
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
|
||||
|
||||
// Remove the IP address from the queue
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
|
||||
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
|
||||
+288
-152
@@ -20,77 +20,111 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
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.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private final @NotNull FallbackPlayer<?, ?> player;
|
||||
private final @NotNull FallbackUser<?, ?> user;
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private final short transactionId;
|
||||
private final long verifyKeepAliveId;
|
||||
private int movementTick, receivedPackets;
|
||||
private double lastX, lastY, lastZ;
|
||||
private final UUID playerUuid;
|
||||
private short expectedTransactionId;
|
||||
private long expectedKeepAliveId;
|
||||
private int expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets;
|
||||
private int ignoredMovementTicks;
|
||||
private double posX, posY, posZ, lastY;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
@Setter
|
||||
private State state;
|
||||
private @NotNull State state = State.LOGIN_ACK;
|
||||
private boolean listenForMovements;
|
||||
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum State {
|
||||
// LOGIN
|
||||
KEEP_ALIVE(false),
|
||||
// PLAY
|
||||
CLIENT_SETTINGS(false),
|
||||
PLUGIN_MESSAGE(false),
|
||||
TRANSACTION(false),
|
||||
// IN-GAME
|
||||
TELEPORT(false),
|
||||
POSITION(true),
|
||||
COLLISIONS(true),
|
||||
// OTHER
|
||||
SUCCESS(false);
|
||||
|
||||
private final boolean canMove;
|
||||
// 1.20.2 configuration state
|
||||
LOGIN_ACK, CONFIGURE,
|
||||
// pre-JOIN ping check
|
||||
KEEP_ALIVE,
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION, COLLISIONS,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackPlayer<?, ?> player,
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid) {
|
||||
this.player = player;
|
||||
final @NotNull UUID playerUuid) {
|
||||
this.user = user;
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
this.transactionId = (short) RANDOM.nextInt();
|
||||
this.verifyKeepAliveId = RANDOM.nextInt();
|
||||
this.state = State.KEEP_ALIVE;
|
||||
this.playerUuid = playerUuid;
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
player.write(new ServerLoginSuccess(username, uuid));
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process
|
||||
initialJoinProcess();
|
||||
}
|
||||
}
|
||||
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
state = State.CONFIGURE;
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(synchronizeRegistry);
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Set decoder state to actually catch all packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
}
|
||||
|
||||
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
|
||||
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
|
||||
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
|
||||
if (decoder != null && encoder != null) {
|
||||
// Update the packet registry state to be able to listen for CONFIG packets
|
||||
decoder.updateRegistry(registry);
|
||||
encoder.updateRegistry(registry);
|
||||
} else {
|
||||
// Something went wrong - the decoder should not be null
|
||||
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
// Close the channel to prevent bypasses or other potential exploits
|
||||
user.getChannel().close();
|
||||
}
|
||||
}
|
||||
|
||||
private void initialJoinProcess() {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
// 1.7 players don't have KeepAlive packets in the login process
|
||||
sendJoinGamePacket();
|
||||
} else {
|
||||
// Set the state to KEEP_ALIVE to avoid false positives
|
||||
state = State.KEEP_ALIVE;
|
||||
// Generate a random KeepAlive ID for the pre-join check
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
// Send first KeepAlive to check if the connection is somewhat responsive
|
||||
player.write(new KeepAlive(verifyKeepAliveId));
|
||||
user.write(new KeepAlive(expectedKeepAliveId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,18 +132,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Set the state to TRANSACTION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TRANSACTION;
|
||||
// Send a transaction with a
|
||||
player.write(new Transaction(
|
||||
0, transactionId, false
|
||||
));
|
||||
// Generate a random transaction ID
|
||||
expectedTransactionId = (short) RANDOM.nextInt();
|
||||
// Send a transaction with the given ID
|
||||
user.write(new Transaction(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
private void sendJoinGamePacket() {
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.CLIENT_SETTINGS;
|
||||
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
|
||||
if (!v1_20_2) {
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.CLIENT_SETTINGS;
|
||||
}
|
||||
// Select the JoinGame packet for the respective protocol version
|
||||
player.write(getJoinPacketForVersion(player.getProtocolVersion()));
|
||||
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
|
||||
if (v1_20_2) {
|
||||
// Perform the transaction check since the ClientSettings
|
||||
// and PluginMessage packets have already been validated.
|
||||
sendTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAbilitiesAndTeleport() {
|
||||
// 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);
|
||||
// Generate the current teleport ID
|
||||
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PositionLook(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, 0f, expectedTeleportId, false
|
||||
));
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
@@ -117,38 +174,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
player.write(EMPTY_CHUNK_DATA);
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
player.write(UPDATE_SECTION_BLOCKS);
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(UPDATE_SECTION_BLOCKS);
|
||||
}
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private static boolean validateClientLocale(final @NotNull FallbackPlayer<?, ?> player, final String locale) {
|
||||
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().VALID_LOCALE_REGEX;
|
||||
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 FallbackPlayer<?, ?> player, 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 {
|
||||
// No need to check for empty brands since ProtocolUtil#readBrandMessage
|
||||
// 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()) {
|
||||
return false;
|
||||
}
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().VALID_BRAND_REGEX;
|
||||
// 1.7 has some very weird issues when trying to decode the client brand
|
||||
final boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0;
|
||||
final int cap = Sonar.get().getConfig().MAXIMUM_BRAND_LENGTH;
|
||||
// Read the client brand using our custom readString method that supports 1.7.
|
||||
// The legacy version of readString does not compare the string length
|
||||
// with the VarInt sent by the client.
|
||||
final String read = ProtocolUtil.readString(content, cap, legacy);
|
||||
// No need to check for empty or too long client brands since
|
||||
// ProtocolUtil#readString already does exactly that.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
|
||||
return !read.equals("Vanilla") // The normal brand is always lowercase
|
||||
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
|
||||
} catch (DecoderException exception) {
|
||||
// Fail if the string (client brand) could not be decoded properly
|
||||
player.fail("could not decode string");
|
||||
user.fail("could not decode string");
|
||||
// Throw the exception so we don't continue checking
|
||||
throw exception;
|
||||
}
|
||||
@@ -156,26 +216,52 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().MAXIMUM_LOGIN_PACKETS
|
||||
+ Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
|
||||
checkFrame(++receivedPackets < maxPackets, "too many packets");
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().VERIFICATION_TIMEOUT;
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
// Check if the time limit has exceeded
|
||||
if (login.delay() > timeout) {
|
||||
player.getChannel().close();
|
||||
if (login.elapsed(timeout)) {
|
||||
user.getChannel().close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof LoginAcknowledged) {
|
||||
// Check if we are currently expecting a LoginAcknowledged packet
|
||||
assertState(State.LOGIN_ACK);
|
||||
|
||||
// Start the configuration process for 1.20.2 clients
|
||||
configure();
|
||||
}
|
||||
|
||||
if (packet instanceof FinishConfiguration) {
|
||||
// Check if we are currently expecting a FinishConfiguration packet
|
||||
assertState(State.CONFIGURE);
|
||||
|
||||
// Check if the client has already sent valid ClientSettings and PluginMessage packets
|
||||
checkFrame(resolvedClientBrand, "did not resolve client brand");
|
||||
checkFrame(resolvedClientSettings, "did not resolve client settings");
|
||||
|
||||
// Start initializing the actual join process
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
initialJoinProcess();
|
||||
}
|
||||
|
||||
if (packet instanceof KeepAlive) {
|
||||
final KeepAlive keepAlive = (KeepAlive) packet;
|
||||
|
||||
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
|
||||
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
|
||||
|
||||
// Check if we are currently expecting a KeepAlive packet
|
||||
assertState(State.KEEP_ALIVE);
|
||||
|
||||
final KeepAlive keepAlive = (KeepAlive) packet;
|
||||
|
||||
checkFrame(keepAlive.getId() == verifyKeepAliveId, "invalid KeepAlive ID");
|
||||
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
|
||||
|
||||
// The correct KeepAlive packet has been received
|
||||
sendJoinGamePacket();
|
||||
@@ -185,35 +271,44 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
final ClientSettings clientSettings = (ClientSettings) packet;
|
||||
|
||||
// Validate the locale using a regex to filter unwanted characters.
|
||||
checkFrame(validateClientLocale(player, clientSettings.getLocale()), "invalid locale");
|
||||
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.CLIENT_SETTINGS) {
|
||||
state = State.PLUGIN_MESSAGE;
|
||||
}
|
||||
|
||||
// Make sure we mark the ClientSettings as valid
|
||||
resolvedClientSettings = true;
|
||||
}
|
||||
|
||||
if (packet instanceof PluginMessage) {
|
||||
final PluginMessage pluginMessage = (PluginMessage) packet;
|
||||
|
||||
// Only the brand channel is important
|
||||
// Only the brand channel is important, drop the rest
|
||||
if (pluginMessage.getChannel().equals("MC|Brand")
|
||||
|| pluginMessage.getChannel().equals("minecraft:brand")) {
|
||||
// Check if the brand packet was sent twice,
|
||||
// which is not possible when using a vanilla Minecraft client.
|
||||
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
|
||||
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = player.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
checkFrame(validateClientBrand(player, pluginMessage.content()), "invalid client brand");
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.PLUGIN_MESSAGE) {
|
||||
// Send the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
// Make sure we mark the PluginMessage as valid
|
||||
resolvedClientBrand = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,27 +321,33 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// The transaction should always be accepted
|
||||
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
|
||||
// Check if the transaction ID is valid
|
||||
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
|
||||
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
|
||||
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().CHECK_GRAVITY) {
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player is unable to fly (the player is in spectator mode)
|
||||
player.write(DEFAULT_ABILITIES);
|
||||
// Teleport the player to the spawn position
|
||||
player.write(SPAWN_TELEPORT);
|
||||
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, send an Abilities packet to the client to make
|
||||
// sure the player falls even in spectator mode.
|
||||
// Then, teleport the player to the spawn position.
|
||||
sendAbilitiesAndTeleport();
|
||||
|
||||
// 1.7-1.8 clients do not have a TeleportConfirm packet
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
|
||||
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
|
||||
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
|
||||
sendChunkData();
|
||||
} else {
|
||||
// Set the state to TELEPORT to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TELEPORT;
|
||||
// Send all previously sent packets in one flush since we didn't flush
|
||||
// the channel earlier when we teleported the player.
|
||||
user.getChannel().flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,47 +355,53 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Check if we are currently expecting a TeleportConfirm packet
|
||||
assertState(State.TELEPORT);
|
||||
|
||||
// Check if the teleport ID is correct
|
||||
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
|
||||
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
|
||||
|
||||
// Check if the teleport id is correct
|
||||
final boolean teleportIdCorrect = teleportConfirm.getTeleportId() == SPAWN_TELEPORT.getTeleportId();
|
||||
checkFrame(teleportIdCorrect, "invalid teleport ID");
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = lastY = -1;
|
||||
expectedTeleportId = -1;
|
||||
|
||||
// Now we can send the chunk data
|
||||
sendChunkData();
|
||||
}
|
||||
|
||||
// Only handle positions if the player can send position packets.
|
||||
if (state.canMove) {
|
||||
if (state != State.LOGIN_ACK) {
|
||||
if (packet instanceof Position) {
|
||||
final Position position = (Position) packet;
|
||||
|
||||
// Immediately handle new position update
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
}
|
||||
|
||||
if (packet instanceof PositionLook) {
|
||||
final PositionLook position = (PositionLook) packet;
|
||||
|
||||
// Immediately handle new position update
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
}
|
||||
|
||||
if (packet instanceof Player) {
|
||||
final Player player = (Player) packet;
|
||||
|
||||
// This packet does not send new position data, reuse the last Y
|
||||
handlePositionUpdate(lastX, lastY, lastZ, player.isOnGround());
|
||||
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
|
||||
final double deltaY = lastY - y;
|
||||
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
|
||||
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) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
expectedTeleportId = -1;
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
lastZ = z;
|
||||
posX = x;
|
||||
lastY = posY;
|
||||
posY = y;
|
||||
posZ = z;
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
@@ -304,56 +411,85 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// The onGround property can never be true when we aren't checking for collisions
|
||||
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
|
||||
|
||||
// Skip teleports using this small check
|
||||
if (deltaY > 0.07) {
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
listenForMovements = true;
|
||||
}
|
||||
|
||||
// Verify the player if they sent correct movement packets
|
||||
if (movementTick++ >= MAX_MOVEMENT_TICK) {
|
||||
if (Sonar.get().getConfig().CHECK_COLLISIONS) {
|
||||
if (state != State.COLLISIONS) {
|
||||
// Set the state to COLLISIONS to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
// Now we don't care about gravity anymore,
|
||||
// we just want the player to collide with the blocks.
|
||||
state = State.COLLISIONS;
|
||||
} else {
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
lastY = dynamicSpawnYPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
// The offset cannot greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
|
||||
final double deltaY = lastY - y;
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Check for too many ignored Y ticks
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
|
||||
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
if (ground) {
|
||||
// The player is colliding, finish verification
|
||||
// TODO: Check for: velocity, entities/mounting, interactions
|
||||
finish();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Checking collisions is disabled, just finish verification
|
||||
finish();
|
||||
}
|
||||
} else if (y >= DEFAULT_Y_COLLIDE_POSITION && y <= DYNAMIC_SPAWN_Y_POSITION) {
|
||||
// This is a basic gravity check that predicts the next y position
|
||||
final double predictedY = PREPARED_MOVEMENT_PACKETS[movementTick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
if (tick > maxMovementTick) {
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Checking collisions is disabled, just finish verification
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the y motion is roughly equal to the predicted value
|
||||
checkFrame(offsetY < 0.01, "invalid offset: " + y + ", " + offsetY);
|
||||
if (state != State.COLLISIONS) {
|
||||
// Now we don't care about gravity anymore,
|
||||
// we just want the player to collide with the blocks.
|
||||
state = State.COLLISIONS;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
|
||||
}
|
||||
|
||||
// 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.
|
||||
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
|
||||
|
||||
if (ground) {
|
||||
// The player is colliding to blocks, finish verification
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ground && tick < maxPredictionTick) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
}
|
||||
|
||||
// Check if the y motion is roughly equal to the predicted value
|
||||
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
|
||||
tick, y, offsetY, deltaY, predictedY));
|
||||
}
|
||||
tick++;
|
||||
}
|
||||
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
if (expectedState != state) {
|
||||
player.fail("expected " + expectedState + ", got " + state);
|
||||
user.fail("expected " + expectedState + ", got " + state);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
if (!condition) {
|
||||
player.fail(message);
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
}
|
||||
@@ -361,21 +497,21 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void finish() {
|
||||
state = State.SUCCESS;
|
||||
|
||||
player.getFallback().getConnected().remove(username);
|
||||
user.getFallback().getConnected().remove(username);
|
||||
|
||||
// Add verified player to the database
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(
|
||||
player.getInetAddress().toString(), uuid, login.getStart()
|
||||
);
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
|
||||
login.getStart());
|
||||
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
|
||||
|
||||
// Disconnect player with the verification success message
|
||||
player.disconnect(Sonar.get().getConfig().VERIFICATION_SUCCESS);
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
|
||||
|
||||
player.getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().VERIFICATION_SUCCESSFUL_LOG
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.formattedDelay())
|
||||
);
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
|
||||
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.toString()));
|
||||
}
|
||||
}
|
||||
+55
-34
@@ -21,79 +21,100 @@ import io.netty.buffer.ByteBuf;
|
||||
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 xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackPlayer<?, ?> connection;
|
||||
private final FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final FallbackPacketListener listener;
|
||||
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackPlayer<?, ?> connection,
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
|
||||
final @NotNull FallbackPacketListener listener) {
|
||||
this.connection = connection;
|
||||
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
|
||||
FallbackPacketRegistry.Direction.SERVERBOUND, connection.getProtocolVersion()
|
||||
);
|
||||
this.user = user;
|
||||
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
|
||||
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
// Release the ByteBuf if the connection is not active
|
||||
// or the ByteBuf doesn't contain any data to avoid
|
||||
// memory leaks or other potential exploits.
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
final int originalReaderIndex = byteBuf.readerIndex();
|
||||
// Read the packet ID and then create the packet from it
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// If the packet hasn't been found, skip it
|
||||
// TODO: Can we fail the verification afterwards?
|
||||
if (packet == null) {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
} else {
|
||||
try {
|
||||
doLengthSanityChecks(byteBuf, packet);
|
||||
|
||||
try {
|
||||
packet.decode(byteBuf, connection.getProtocolVersion());
|
||||
} catch (Throwable throwable) {
|
||||
connection.fail("could not decode packet");
|
||||
throw new CorruptedFrameException("Failed to decode packet");
|
||||
}
|
||||
|
||||
if (byteBuf.isReadable()) {
|
||||
connection.fail("could not read packet to end");
|
||||
throw new CorruptedFrameException("Could not read packet to end");
|
||||
}
|
||||
|
||||
listener.handle(packet);
|
||||
} finally {
|
||||
byteBuf.release();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure that the packet isn't too large or too small
|
||||
doLengthSanityChecks(byteBuf, packet);
|
||||
|
||||
try {
|
||||
// Try to decode the packet for the given protocol version
|
||||
packet.decode(byteBuf, user.getProtocolVersion());
|
||||
} catch (Throwable throwable) {
|
||||
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
|
||||
throw new CorruptedFrameException("Failed to decode packet");
|
||||
}
|
||||
|
||||
// Check if the packet still has bytes left after we decoded it
|
||||
if (byteBuf.isReadable()) {
|
||||
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
|
||||
throw new CorruptedFrameException("Could not read packet to end");
|
||||
}
|
||||
|
||||
// Let our verification handler process the packet
|
||||
listener.handle(packet);
|
||||
} finally {
|
||||
// Release the ByteBuf to avoid memory leaks
|
||||
byteBuf.release();
|
||||
}
|
||||
} else {
|
||||
// Packets are always ByteBufs - if we detect an unknown object, fail verification
|
||||
user.fail("packet is not a ByteBuf");
|
||||
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
|
||||
}
|
||||
}
|
||||
|
||||
private void doLengthSanityChecks(final ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, connection.getProtocolVersion());
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
|
||||
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
|
||||
connection.fail("packet too large");
|
||||
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
|
||||
throw new CorruptedFrameException("Packet too large");
|
||||
}
|
||||
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, connection.getProtocolVersion());
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
|
||||
if (byteBuf.readableBytes() < expectedMinLen) {
|
||||
connection.fail("packet too small");
|
||||
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
|
||||
throw new CorruptedFrameException("Packet too small");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -20,19 +20,24 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
|
||||
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
|
||||
FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion
|
||||
);
|
||||
updateRegistry(LOGIN);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+89
-54
@@ -22,7 +22,11 @@ import io.netty.util.collection.IntObjectMap;
|
||||
import lombok.Data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
@@ -32,16 +36,38 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
// Most of this is taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
|
||||
public enum FallbackPacketRegistry {
|
||||
SONAR {
|
||||
LOGIN {
|
||||
{
|
||||
clientbound.fallback = false;
|
||||
serverbound.fallback = false;
|
||||
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
|
||||
// LOGIN
|
||||
clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
|
||||
clientbound.register(LoginSuccess.class, LoginSuccess::new,
|
||||
map(0x02, MINECRAFT_1_7_2, false));
|
||||
}
|
||||
},
|
||||
CONFIG {
|
||||
{
|
||||
clientbound.register(Disconnect.class, Disconnect::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(RegistrySync.class, RegistrySync::new,
|
||||
map(0x05, MINECRAFT_1_20_2, false));
|
||||
|
||||
// GAME
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PluginMessage.class, PluginMessage::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
}
|
||||
},
|
||||
GAME {
|
||||
{
|
||||
clientbound.register(JoinGame.class, JoinGame::new,
|
||||
map(0x01, MINECRAFT_1_7_2, true),
|
||||
map(0x23, MINECRAFT_1_9, true),
|
||||
@@ -54,7 +80,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x23, MINECRAFT_1_19, true),
|
||||
map(0x25, MINECRAFT_1_19_1, true),
|
||||
map(0x24, MINECRAFT_1_19_3, true),
|
||||
map(0x28, MINECRAFT_1_19_4, true));
|
||||
map(0x28, MINECRAFT_1_19_4, true),
|
||||
map(0x29, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x1F, MINECRAFT_1_9, false),
|
||||
@@ -67,7 +94,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1E, MINECRAFT_1_19, false),
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x23, MINECRAFT_1_19_4, false));
|
||||
map(0x23, MINECRAFT_1_19_4, false),
|
||||
map(0x24, MINECRAFT_1_20_2, false));
|
||||
clientbound.register(Disconnect.class, Disconnect::new,
|
||||
map(0x40, MINECRAFT_1_7_2, true),
|
||||
map(0x1A, MINECRAFT_1_9, true),
|
||||
@@ -80,7 +108,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x17, MINECRAFT_1_19, true),
|
||||
map(0x19, MINECRAFT_1_19_1, true),
|
||||
map(0x17, MINECRAFT_1_19_3, true),
|
||||
map(0x1A, MINECRAFT_1_19_4, true));
|
||||
map(0x1A, MINECRAFT_1_19_4, true),
|
||||
map(0x1B, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(PositionLook.class, PositionLook::new,
|
||||
map(0x08, MINECRAFT_1_7_2, true),
|
||||
map(0x2E, MINECRAFT_1_9, true),
|
||||
@@ -94,7 +123,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x36, MINECRAFT_1_19, true),
|
||||
map(0x39, MINECRAFT_1_19_1, true),
|
||||
map(0x38, MINECRAFT_1_19_3, true),
|
||||
map(0x3C, MINECRAFT_1_19_4, true));
|
||||
map(0x3C, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(Abilities.class, Abilities::new,
|
||||
map(0x39, MINECRAFT_1_7_2, true),
|
||||
map(0x2B, MINECRAFT_1_9, true),
|
||||
@@ -108,7 +138,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2f, MINECRAFT_1_19, true),
|
||||
map(0x31, MINECRAFT_1_19_1, true),
|
||||
map(0x30, MINECRAFT_1_19_3, true),
|
||||
map(0x34, MINECRAFT_1_19_4, true));
|
||||
map(0x34, MINECRAFT_1_19_4, true),
|
||||
map(0x36, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
|
||||
map(0x21, MINECRAFT_1_7_2, true),
|
||||
map(0x20, MINECRAFT_1_9, true),
|
||||
@@ -121,33 +152,21 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1F, MINECRAFT_1_19, true),
|
||||
map(0x21, MINECRAFT_1_19_1, true),
|
||||
map(0x20, MINECRAFT_1_19_3, true),
|
||||
map(0x24, MINECRAFT_1_19_4, true));
|
||||
map(0x24, MINECRAFT_1_19_4, true),
|
||||
map(0x25, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
|
||||
map(34, MINECRAFT_1_7_2, true),
|
||||
map(16, MINECRAFT_1_9, true),
|
||||
map(15, MINECRAFT_1_13, true),
|
||||
map(16, MINECRAFT_1_15, true),
|
||||
map(15, MINECRAFT_1_16, true),
|
||||
map(59, MINECRAFT_1_16_2, true),
|
||||
map(63, MINECRAFT_1_17, true),
|
||||
map(61, MINECRAFT_1_19, true),
|
||||
map(64, MINECRAFT_1_19_1, true),
|
||||
map(63, MINECRAFT_1_19_3, true),
|
||||
map(67, MINECRAFT_1_19_4, true));
|
||||
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
map(0x45, MINECRAFT_1_12, true),
|
||||
map(0x46, MINECRAFT_1_12_1, true),
|
||||
map(0x49, MINECRAFT_1_13, true),
|
||||
map(0x4D, MINECRAFT_1_14, true),
|
||||
map(0x4E, MINECRAFT_1_15, true),
|
||||
map(0x42, MINECRAFT_1_16, true),
|
||||
map(0x4B, MINECRAFT_1_17, true),
|
||||
map(0x4A, MINECRAFT_1_19, true),
|
||||
map(0x4D, MINECRAFT_1_19_1, true),
|
||||
map(0x4C, MINECRAFT_1_19_3, true),
|
||||
map(0x50, MINECRAFT_1_19_4, true));
|
||||
map(0x22, MINECRAFT_1_7_2, true),
|
||||
map(0x10, MINECRAFT_1_9, true),
|
||||
map(0x0F, MINECRAFT_1_13, true),
|
||||
map(0x10, MINECRAFT_1_15, true),
|
||||
map(0x0F, MINECRAFT_1_16, true),
|
||||
map(0x3B, MINECRAFT_1_16_2, true),
|
||||
map(0x3F, MINECRAFT_1_17, true),
|
||||
map(0x3D, MINECRAFT_1_19, true),
|
||||
map(0x40, MINECRAFT_1_19_1, true),
|
||||
map(0x3F, MINECRAFT_1_19_3, true),
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(Transaction.class, Transaction::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -159,7 +178,20 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2D, MINECRAFT_1_19, true),
|
||||
map(0x2F, MINECRAFT_1_19_1, true),
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true));
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(Chat.class, Chat::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true),
|
||||
map(0x0F, MINECRAFT_1_9, true),
|
||||
map(0x0E, MINECRAFT_1_13, true),
|
||||
map(0x0F, MINECRAFT_1_15, true),
|
||||
map(0x0E, MINECRAFT_1_16, true),
|
||||
map(0x0F, MINECRAFT_1_18_2, true),
|
||||
map(0x5F, MINECRAFT_1_19, true),
|
||||
map(0x62, MINECRAFT_1_19_1, true),
|
||||
map(0x60, MINECRAFT_1_19_3, true),
|
||||
map(0x64, MINECRAFT_1_19_4, true),
|
||||
map(0x67, MINECRAFT_1_20_2, true));
|
||||
|
||||
serverbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
@@ -173,7 +205,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x11, MINECRAFT_1_19, false),
|
||||
map(0x12, MINECRAFT_1_19_1, false),
|
||||
map(0x11, MINECRAFT_1_19_3, false),
|
||||
map(0x12, MINECRAFT_1_19_4, false));
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
@@ -183,7 +216,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x07, MINECRAFT_1_19, false),
|
||||
map(0x08, MINECRAFT_1_19_1, false),
|
||||
map(0x07, MINECRAFT_1_19_3, false),
|
||||
map(0x08, MINECRAFT_1_19_4, false));
|
||||
map(0x08, MINECRAFT_1_19_4, false),
|
||||
map(0x09, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PluginMessage.class, PluginMessage::new,
|
||||
map(0x17, MINECRAFT_1_7_2, false),
|
||||
map(0x09, MINECRAFT_1_9, false),
|
||||
@@ -195,7 +229,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x0C, MINECRAFT_1_19, false),
|
||||
map(0x0D, MINECRAFT_1_19_1, false),
|
||||
map(0x0C, MINECRAFT_1_19_3, false),
|
||||
map(0x0D, MINECRAFT_1_19_4, false));
|
||||
map(0x0D, MINECRAFT_1_19_4, false),
|
||||
map(0x0F, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PositionLook.class, PositionLook::new,
|
||||
map(0x06, MINECRAFT_1_7_2, false),
|
||||
map(0x0D, MINECRAFT_1_9, false),
|
||||
@@ -208,7 +243,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_19, false),
|
||||
map(0x15, MINECRAFT_1_19_1, false),
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false));
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(Position.class, Position::new,
|
||||
@@ -223,7 +259,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x13, MINECRAFT_1_19, false),
|
||||
map(0x14, MINECRAFT_1_19_1, false),
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false));
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(Player.class, Player::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
@@ -236,7 +273,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x16, MINECRAFT_1_19, false),
|
||||
map(0x17, MINECRAFT_1_19_1, false),
|
||||
map(0x16, MINECRAFT_1_19_3, false),
|
||||
map(0x17, MINECRAFT_1_19_4, false));
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(Transaction.class, Transaction::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -248,7 +286,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1F, MINECRAFT_1_19, false),
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x20, MINECRAFT_1_19_4, false));
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,7 +306,6 @@ public enum FallbackPacketRegistry {
|
||||
|
||||
public static class PacketRegistry {
|
||||
private final Map<ProtocolVersion, ProtocolRegistry> versions;
|
||||
private boolean fallback = true;
|
||||
|
||||
PacketRegistry() {
|
||||
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
|
||||
@@ -283,9 +321,6 @@ public enum FallbackPacketRegistry {
|
||||
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
|
||||
final ProtocolRegistry registry = versions.get(version);
|
||||
if (registry == null) {
|
||||
if (fallback) {
|
||||
return getProtocolRegistry(MINIMUM_VERSION);
|
||||
}
|
||||
throw new IllegalArgumentException("Could not find data for protocol version " + version);
|
||||
}
|
||||
return registry;
|
||||
@@ -386,7 +421,7 @@ public enum FallbackPacketRegistry {
|
||||
return supplier.get();
|
||||
}
|
||||
|
||||
public int getPacketId(final FallbackPacket packet) {
|
||||
public int getPacketId(final @NotNull FallbackPacket packet) {
|
||||
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
|
||||
|
||||
if (id == Integer.MIN_VALUE) {
|
||||
@@ -414,9 +449,9 @@ public enum FallbackPacketRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
private static PacketMapping map(final int id,
|
||||
final ProtocolVersion version,
|
||||
final boolean encodeOnly) {
|
||||
private static @NotNull PacketMapping map(final int id,
|
||||
final ProtocolVersion version,
|
||||
final boolean encodeOnly) {
|
||||
return new PacketMapping(id, version, null, encodeOnly);
|
||||
}
|
||||
}
|
||||
+85
-110
@@ -24,106 +24,101 @@ import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
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.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
private final Random RANDOM = new Random();
|
||||
private final DimensionInfo DIMENSION = new DimensionInfo(
|
||||
"minecraft:overworld",
|
||||
"sonar", false, false
|
||||
);
|
||||
|
||||
// Mappings
|
||||
private final CompoundBinaryTag CHAT_TYPE_119;
|
||||
private final CompoundBinaryTag CHAT_TYPE_1191;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_1194;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_120;
|
||||
|
||||
static {
|
||||
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
|
||||
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
|
||||
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
|
||||
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
|
||||
DAMAGE_TYPE_1194 = getMapping("damage_type_1194.nbt");
|
||||
DAMAGE_TYPE_120 = getMapping("damage_type_120.nbt");
|
||||
}
|
||||
|
||||
// JoinGame
|
||||
public JoinGame LEGACY_JOIN_GAME;
|
||||
private JoinGame JOIN_GAME_1_16;
|
||||
private JoinGame JOIN_GAME_1_16_2;
|
||||
private JoinGame JOIN_GAME_1_18_2;
|
||||
private JoinGame JOIN_GAME_1_19_1;
|
||||
private JoinGame JOIN_GAME_1_19_4;
|
||||
private JoinGame JOIN_GAME_1_20;
|
||||
|
||||
// Abilities
|
||||
public final Abilities DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
|
||||
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
|
||||
// Chunks
|
||||
public final EmptyChunkData EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
|
||||
public PositionLook SPAWN_TELEPORT;
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
|
||||
// Registry Sync
|
||||
public FallbackPacket synchronizeRegistry;
|
||||
|
||||
// JoinGame
|
||||
private FallbackPacket legacyJoinGame;
|
||||
private FallbackPacket joinGame116;
|
||||
private FallbackPacket joinGame1162;
|
||||
private FallbackPacket joinGame1182;
|
||||
private FallbackPacket joinGame1191;
|
||||
private FallbackPacket joinGame1194;
|
||||
private JoinGame joinGame120;
|
||||
|
||||
// Dimension
|
||||
private final DimensionInfo DIMENSION = new DimensionInfo(
|
||||
"minecraft:overworld", "sonar", false, false);
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
|
||||
private final int SPAWN_BUFFER = 10; // player spawns at 255 + 10 (10 blocks above the platform)
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is max
|
||||
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
|
||||
|
||||
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
public int MAX_MOVEMENT_TICK;
|
||||
public double[] PREPARED_MOVEMENT_PACKETS;
|
||||
public int DYNAMIC_SPAWN_BUFFER = SPAWN_BUFFER;
|
||||
public int DYNAMIC_SPAWN_Y_POSITION;
|
||||
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
|
||||
|
||||
public void prepare() {
|
||||
LEGACY_JOIN_GAME = createLegacyJoinGamePacket();
|
||||
JOIN_GAME_1_16 = createJoinGamePacket(MINECRAFT_1_16);
|
||||
JOIN_GAME_1_16_2 = createJoinGamePacket(MINECRAFT_1_16_2);
|
||||
JOIN_GAME_1_18_2 = createJoinGamePacket(MINECRAFT_1_18_2);
|
||||
JOIN_GAME_1_19_1 = createJoinGamePacket(MINECRAFT_1_19_1);
|
||||
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
|
||||
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
|
||||
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
|
||||
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
|
||||
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
|
||||
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
|
||||
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
|
||||
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
|
||||
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
|
||||
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
|
||||
|
||||
MAX_MOVEMENT_TICK = Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
|
||||
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
|
||||
maxPredictionTick = maxMovementTick + 10;
|
||||
preparedCachedYMotions = new double[maxPredictionTick + 1];
|
||||
|
||||
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
|
||||
PREPARED_MOVEMENT_PACKETS[i] = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
for (int i = 0; i < maxPredictionTick + 1; i++) {
|
||||
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
}
|
||||
|
||||
// Adjust block and collide Y position based on max movement ticks
|
||||
double maxFallDistance = 0;
|
||||
for (final double motion : PREPARED_MOVEMENT_PACKETS) {
|
||||
maxFallDistance += motion;
|
||||
// Adjust block and collide Y position based on max fall distance
|
||||
maxFallDistance = 0;
|
||||
for (int i = 0; i < maxMovementTick; i++) {
|
||||
maxFallDistance += preparedCachedYMotions[i];
|
||||
}
|
||||
|
||||
// Set the dynamic spawn buffer
|
||||
DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
|
||||
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
DYNAMIC_SPAWN_Y_POSITION = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
|
||||
|
||||
// Prepare spawn PositionLook with the dynamic Y position
|
||||
SPAWN_TELEPORT = new PositionLook(
|
||||
8, DYNAMIC_SPAWN_Y_POSITION, 8,
|
||||
0f, 0f,
|
||||
RANDOM.nextInt(Short.MAX_VALUE),
|
||||
true
|
||||
);
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
|
||||
|
||||
// Prepare collision platform positions
|
||||
int index = 0;
|
||||
@@ -135,7 +130,7 @@ public class FallbackPreparer {
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0
|
||||
);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.STONE);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,60 +140,50 @@ public class FallbackPreparer {
|
||||
|
||||
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
|
||||
return BinaryTagIO.unlimitedReader().read(Objects.requireNonNull(inputStream),
|
||||
BinaryTagIO.Compression.GZIP
|
||||
);
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mapping {}", fileName);
|
||||
throwable.printStackTrace(System.err);
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static JoinGame getJoinPacketForVersion(final ProtocolVersion protocolVersion) {
|
||||
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return LEGACY_JOIN_GAME; // 1.7-1.15.2
|
||||
return legacyJoinGame; // 1.7.2-1.15.2
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return JOIN_GAME_1_16; // 1.16-1.16.1
|
||||
return joinGame116; // 1.16-1.16.1
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
|
||||
return JOIN_GAME_1_16_2; // 1.16.2-1.18
|
||||
return joinGame1162; // 1.16.2-1.18
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
|
||||
return JOIN_GAME_1_18_2; // 1.18.1-1.19
|
||||
return joinGame1182; // 1.18.1-1.19
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return JOIN_GAME_1_19_1; // 1.19.1-1.19.3
|
||||
return joinGame1191; // 1.19.1-1.19.3
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
|
||||
return JOIN_GAME_1_19_4; // 1.19.4
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
return joinGame1194; // 1.19.4
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
return JOIN_GAME_1_20; // 1.20-1.20.1
|
||||
}
|
||||
throw new IllegalStateException("Unsupported protocol version");
|
||||
}
|
||||
|
||||
private @NotNull JoinGame createLegacyJoinGamePacket() {
|
||||
final JoinGame joinGame = new JoinGame();
|
||||
|
||||
joinGame.setLevelType("flat");
|
||||
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
|
||||
joinGame.setDimension(0);
|
||||
joinGame.setReducedDebugInfo(true);
|
||||
return joinGame;
|
||||
return joinGame120; // 1.20-latest
|
||||
}
|
||||
|
||||
// Partially taken from
|
||||
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
|
||||
private @NotNull JoinGame createJoinGamePacket(final ProtocolVersion protocolVersion) {
|
||||
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
|
||||
final JoinGame joinGame = new JoinGame();
|
||||
|
||||
joinGame.setLevelType("flat");
|
||||
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
|
||||
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
|
||||
joinGame.setDimension(0);
|
||||
joinGame.setReducedDebugInfo(true);
|
||||
|
||||
// 1.7.2-1.8.9 don't need any dimension information
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
|
||||
return joinGame;
|
||||
}
|
||||
|
||||
joinGame.setDimensionInfo(DIMENSION);
|
||||
|
||||
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
|
||||
@@ -206,7 +191,7 @@ public class FallbackPreparer {
|
||||
.add(createDimensionData(protocolVersion))
|
||||
.build();
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
|
||||
|
||||
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
|
||||
@@ -245,34 +230,24 @@ public class FallbackPreparer {
|
||||
))).build()
|
||||
);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) == 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
|
||||
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_4) == 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
|
||||
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
|
||||
}
|
||||
} else {
|
||||
registryContainer.put("dimension", encodedDimensionRegistry);
|
||||
}
|
||||
|
||||
try {
|
||||
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
|
||||
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
currentDimensionData = currentDimensionData.getCompound("element");
|
||||
}
|
||||
|
||||
joinGame.setCurrentDimensionData(currentDimensionData);
|
||||
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
|
||||
joinGame.setRegistry(registryContainer.build());
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
currentDimensionData = currentDimensionData.getCompound("element");
|
||||
}
|
||||
|
||||
joinGame.setCurrentDimensionData(currentDimensionData);
|
||||
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
|
||||
joinGame.setRegistry(registryContainer.build());
|
||||
return joinGame;
|
||||
}
|
||||
|
||||
@@ -289,7 +264,7 @@ public class FallbackPreparer {
|
||||
.putBoolean("respawn_anchor_works", false)
|
||||
.putBoolean("has_raids", false)
|
||||
.putInt("logical_height", 256)
|
||||
.putString("infiniburn", version.compareTo(ProtocolVersion.MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
|
||||
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
|
||||
":infiniburn_nether" : "minecraft:infiniburn_nether")
|
||||
.putDouble("coordinate_scale", 1.0)
|
||||
.putString("effects", "minecraft:overworld")
|
||||
@@ -299,7 +274,7 @@ public class FallbackPreparer {
|
||||
.putInt("monster_spawn_light_level", 0)
|
||||
.build();
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
return CompoundBinaryTag.builder()
|
||||
.putString("name", "minecraft:overworld")
|
||||
.putInt("id", 0)
|
||||
|
||||
+57
-9
@@ -17,21 +17,69 @@
|
||||
|
||||
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;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Getter
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum BlockType {
|
||||
STONE {
|
||||
@Override
|
||||
public int getId(final ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(MINECRAFT_1_13) >= 0 ? 1 : 16;
|
||||
STONE(protocolVersion -> 1),
|
||||
BARRIER(protocolVersion -> {
|
||||
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
|
||||
switch (protocolVersion) {
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
|
||||
return 20;
|
||||
default:
|
||||
// 1.8-1.12.2
|
||||
return 166;
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
return 6493;
|
||||
case MINECRAFT_1_13_2:
|
||||
return 6494;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
return 7000;
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
return 7536;
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 7540;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7754;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
return 8245;
|
||||
case MINECRAFT_1_19_3:
|
||||
return 9889;
|
||||
case MINECRAFT_1_19_4:
|
||||
return 10221;
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
public abstract int getId(final ProtocolVersion protocolVersion);
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+11
-19
@@ -15,34 +15,26 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class Position implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
z = byteBuf.readDouble();
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
public final class FinishConfiguration implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+15
-16
@@ -15,42 +15,41 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
|
||||
import java.io.IOException;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class DefaultSpawnPosition implements FallbackPacket {
|
||||
private int x, y, z;
|
||||
private float angle;
|
||||
public final class RegistrySync implements FallbackPacket {
|
||||
private CompoundBinaryTag dimensionRegistry;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
|
||||
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
|
||||
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
|
||||
|
||||
byteBuf.writeLong(encoded);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
byteBuf.writeFloat(angle);
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
|
||||
stream.writeByte(10); // CompoundTag ID
|
||||
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Cannot write NBT CompoundTag");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+11
-16
@@ -15,24 +15,19 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.server;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
|
||||
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.Optional;
|
||||
public final class LoginAcknowledged implements FallbackPacket {
|
||||
|
||||
public interface ServerWrapper {
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Platform of the server (Velocity, BungeeCord, Spigot)
|
||||
*/
|
||||
SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @param username Username of the player
|
||||
* @return Optional player wrapped as InvocationSender
|
||||
* @see xyz.jonesdev.sonar.api.command.InvocationSender
|
||||
*/
|
||||
Optional<InvocationSender> getOnlinePlayer(final String username);
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -15,12 +15,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
|
||||
@@ -34,12 +35,12 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ServerLoginSuccess implements FallbackPacket {
|
||||
public final class LoginSuccess implements FallbackPacket {
|
||||
private String username;
|
||||
private UUID uuid;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeUUID(byteBuf, uuid);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
|
||||
+4
-3
@@ -15,13 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -34,14 +35,14 @@ public class Abilities implements FallbackPacket {
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(encodedFlags);
|
||||
byteBuf.writeFloat(flySpeed);
|
||||
byteBuf.writeFloat(walkSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
encodedFlags = byteBuf.readByte();
|
||||
flySpeed = byteBuf.readFloat();
|
||||
walkSpeed = byteBuf.readFloat();
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class Chat implements FallbackPacket {
|
||||
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
|
||||
private Component component;
|
||||
private byte position;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
writeString(byteBuf, serialized);
|
||||
|
||||
// Message type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(position == 2);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, position);
|
||||
} else {
|
||||
byteBuf.writeByte(this.position);
|
||||
}
|
||||
|
||||
// UUID
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, PLACEHOLDER_UUID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -15,13 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -55,7 +56,7 @@ public final class ClientSettings implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
locale = readString(byteBuf, 16);
|
||||
viewDistance = byteBuf.readByte();
|
||||
chatVisibility = readVarInt(byteBuf);
|
||||
+11
-9
@@ -15,20 +15,22 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.readString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
|
||||
@Getter
|
||||
@@ -38,17 +40,17 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
|
||||
public final class Disconnect implements FallbackPacket {
|
||||
private @Nullable String reason;
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
reason = readString(byteBuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
writeString(byteBuf, Objects.requireNonNull(reason));
|
||||
}
|
||||
|
||||
public static Disconnect create(final String serialized) {
|
||||
return new Disconnect(serialized);
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static @NotNull Disconnect create(final Component component) {
|
||||
return new Disconnect(JSONComponentSerializer.json().serialize(component));
|
||||
}
|
||||
}
|
||||
+25
-31
@@ -15,28 +15,26 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.LongArrayBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
// Partially taken from
|
||||
// https://github.com/Leymooo/BungeeCord/blob/master/protocol/src/main/java/ru/leymooo/botfilter/packets/EmptyChunkPacket.java
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class EmptyChunkData implements FallbackPacket {
|
||||
@@ -45,8 +43,9 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
|
||||
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
|
||||
|
||||
private static final byte[] LEGACY_FILLER_BYTES_17 = new byte[2];
|
||||
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
|
||||
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
|
||||
private static final byte[] MODERN_FILLER_BYTES = new byte[256 * 4];
|
||||
|
||||
// Prepare nbt for 1.18 and pre-1.18
|
||||
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
|
||||
@@ -69,36 +68,26 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(z);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
|
||||
writeVarInt(byteBuf, 0); // mask
|
||||
}
|
||||
} else {
|
||||
byteBuf.writeBoolean(true); // full chunk
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0 && protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
|
||||
byteBuf.writeBoolean(true); // ignore old data
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) == 0) {
|
||||
byteBuf.writeShort(1);
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
final BitSet bitSet = new BitSet();
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
bitSet.set(i, false);
|
||||
}
|
||||
|
||||
long[] mask = bitSet.toLongArray();
|
||||
writeVarInt(byteBuf, mask.length);
|
||||
|
||||
for (long l : mask) {
|
||||
byteBuf.writeLong(l);
|
||||
} else {
|
||||
byteBuf.writeShort(1); // fix void chunk
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +110,12 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
|
||||
} else {
|
||||
byteBuf.writeInt(0); // compressed size
|
||||
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
|
||||
}
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
|
||||
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
+67
-43
@@ -15,26 +15,22 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@@ -49,81 +45,79 @@ public final class JoinGame implements FallbackPacket {
|
||||
private int viewDistance; // 1.14+
|
||||
private boolean reducedDebugInfo;
|
||||
private boolean showRespawnScreen;
|
||||
private boolean limitedCrafting; // 1.20.2+
|
||||
private String[] levelNames; // 1.16+
|
||||
private CompoundBinaryTag registry; // 1.16+
|
||||
private DimensionInfo dimensionInfo; // 1.16+
|
||||
private CompoundBinaryTag currentDimensionData; // 1.16.2+
|
||||
private short previousGamemode; // 1.16+
|
||||
private int simulationDistance; // 1.18+
|
||||
private int portalCooldown; // 1.20+
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
|
||||
// haha funny, they made 1.20.2 more complicated
|
||||
encode1202Up(byteBuf, protocolVersion);
|
||||
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
|
||||
// so separate it out.
|
||||
encode116Up(byteBuf, protocolVersion);
|
||||
} else {
|
||||
encodeLegacy(byteBuf, protocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeLegacy(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
|
||||
byteBuf.writeInt(dimension);
|
||||
} else {
|
||||
byteBuf.writeByte(dimension);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
|
||||
byteBuf.writeByte(difficulty);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
}
|
||||
|
||||
byteBuf.writeByte(1); // max players
|
||||
|
||||
writeString(byteBuf, Objects.requireNonNull(levelType));
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
if (levelType == null) {
|
||||
throw new IllegalStateException("No level type specified.");
|
||||
}
|
||||
writeString(byteBuf, levelType);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private void encode116Up(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
writeCompoundTag(byteBuf, currentDimensionData);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
} else {
|
||||
@@ -132,15 +126,14 @@ public final class JoinGame implements FallbackPacket {
|
||||
}
|
||||
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
} else {
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeByte(0); // max players
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
}
|
||||
|
||||
@@ -150,16 +143,47 @@ public final class JoinGame implements FallbackPacket {
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
// no last death location
|
||||
byteBuf.writeBoolean(false);
|
||||
// optional death location
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
|
||||
writeVarInt(byteBuf, portalCooldown);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
}
|
||||
}
|
||||
|
||||
private void encode1202Up(final @NotNull ByteBuf byteBuf,
|
||||
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(limitedCrafting);
|
||||
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
+6
-3
@@ -15,12 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -30,13 +32,14 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class KeepAlive implements FallbackPacket {
|
||||
private long id;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
|
||||
byteBuf.writeLong(id);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
@@ -47,7 +50,7 @@ public final class KeepAlive implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
|
||||
id = byteBuf.readLong();
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
+3
-2
@@ -15,13 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -33,7 +34,7 @@ public final class Player implements FallbackPacket {
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
+8
-2
@@ -15,11 +15,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
|
||||
@@ -45,7 +46,12 @@ public final class PluginMessage extends DeferredByteBufHolder implements Fallba
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0xfff + 1; // limit packet size to 4096
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
channel = readString(byteBuf);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class Position implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@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();
|
||||
z = byteBuf.readDouble();
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
+30
-9
@@ -15,13 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -39,8 +40,12 @@ public final class PositionLook implements FallbackPacket {
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
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();
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
@@ -49,20 +54,36 @@ public final class PositionLook implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeDouble(x);
|
||||
byteBuf.writeDouble(y);
|
||||
byteBuf.writeDouble(z);
|
||||
byteBuf.writeFloat(yaw);
|
||||
byteBuf.writeFloat(pitch);
|
||||
byteBuf.writeByte(0x00);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
|
||||
writeVarInt(byteBuf, teleportId);
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeBoolean(onGround);
|
||||
} else {
|
||||
byteBuf.writeByte(0x00);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0 && protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
byteBuf.writeBoolean(true); // Dismount vehicle
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
|
||||
writeVarInt(byteBuf, teleportId);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
byteBuf.writeBoolean(true); // Always dismount vehicle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 33;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
+5
-4
@@ -15,13 +15,14 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@@ -36,12 +37,12 @@ public final class Transaction implements FallbackPacket {
|
||||
private boolean accepted;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeShort((short) id);
|
||||
// The "accepted" field is actually really unnecessary since
|
||||
// the client must accept the transaction either way.
|
||||
// it's never even used in the client.
|
||||
byteBuf.writeBoolean(accepted);
|
||||
} else {
|
||||
byteBuf.writeInt(id);
|
||||
@@ -49,7 +50,7 @@ public final class Transaction implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
windowId = byteBuf.readByte();
|
||||
id = byteBuf.readShort();
|
||||
+27
-16
@@ -15,51 +15,61 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_16_2;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarLong;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString(exclude = {"changedBlocks"})
|
||||
public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
private int sectionX, sectionZ;
|
||||
private ChangedBlock[] changedBlocks;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
|
||||
byteBuf.writeInt(sectionX);
|
||||
byteBuf.writeInt(sectionZ);
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(changedBlocks.length);
|
||||
byteBuf.writeInt(4 * changedBlocks.length);
|
||||
} else {
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
}
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
|
||||
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
|
||||
final int id = block.getType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
writeVarInt(byteBuf, id);
|
||||
} else {
|
||||
final int shiftedBlockId = id << 4;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(shiftedBlockId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, shiftedBlockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
|
||||
final int sectionY = lastChangedBlock.getPosition().getY() >> 4;
|
||||
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
|
||||
|
||||
// Why is Mojang doing this? :(
|
||||
byteBuf.writeLong(
|
||||
((long) (sectionX & 0x3FFFFF) << 42)
|
||||
| (sectionY & 0xFFFFF)
|
||||
| ((long) (sectionZ & 0x3FFFFF) << 20)
|
||||
);
|
||||
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
|
||||
|
||||
// 1.20+ don't have light update suppression
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) < 0) {
|
||||
@@ -70,7 +80,8 @@ public final class UpdateSectionBlocks implements FallbackPacket {
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
writeVarLong(byteBuf, shiftedBlockId | block.getModernChunkPosCrammed());
|
||||
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
|
||||
writeVarLong(byteBuf, positionIdValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -27,13 +27,11 @@ public class TrafficChannelHooker implements FallbackPipelines {
|
||||
public void hook(final @NotNull ChannelPipeline pipeline,
|
||||
final @NotNull String decoder,
|
||||
final @NotNull String encoder) {
|
||||
pipeline.channel().eventLoop().execute(() -> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-52
@@ -25,7 +25,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "blacklist",
|
||||
@@ -40,85 +39,60 @@ import java.net.UnknownHostException;
|
||||
public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "blacklist add <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawInetAddress);
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_DUPLICATE);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD_WARNING
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "blacklist remove <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawInetAddress);
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_NOT_FOUND);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_REMOVE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
}
|
||||
@@ -127,21 +101,19 @@ public final class BlacklistCommand extends Subcommand {
|
||||
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
|
||||
|
||||
if (blacklisted == 0) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_EMPTY);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
SONAR.getFallback().getBlacklisted().invalidateAll();
|
||||
|
||||
invocation.getSender().sendMessage(
|
||||
SONAR.getConfig().BLACKLIST_CLEARED
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_SIZE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
|
||||
break;
|
||||
}
|
||||
|
||||
+76
-17
@@ -17,14 +17,23 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
// This command is only used to display helpful information in order to help
|
||||
// the Sonar developer(s) to fix issues more quickly
|
||||
// the Sonar contributors to fix issues more quickly
|
||||
@SubcommandInfo(
|
||||
name = "dump",
|
||||
description = "Print developer information",
|
||||
@@ -33,22 +42,72 @@ import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
public final class DumpCommand extends Subcommand implements JVMProfiler {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String osName = System.getProperty("os.name");
|
||||
final String osArch = System.getProperty("os.arch");
|
||||
final String jvmArgs = String.join(",", RUNTIME_MX_BEAN.getInputArguments());
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
|
||||
SONAR.getLogger().info("Generated dump: {}", json);
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage("Virtual cores: " + getVirtualCores());
|
||||
invocation.getSender().sendMessage("JVM args: " + jvmArgs);
|
||||
invocation.getSender().sendMessage("Java name: " + RUNTIME_MX_BEAN.getVmName());
|
||||
invocation.getSender().sendMessage("Java version: " + RUNTIME_MX_BEAN.getVmVersion());
|
||||
invocation.getSender().sendMessage("Java vendor: " + RUNTIME_MX_BEAN.getVmVendor());
|
||||
invocation.getSender().sendMessage("Operating system: " + osName + ", " + osArch);
|
||||
invocation.getSender().sendMessage("Total memory: " + formatMemory(getTotalMemory()));
|
||||
invocation.getSender().sendMessage("Max memory: " + formatMemory(getMaxMemory()));
|
||||
invocation.getSender().sendMessage("Free memory: " + formatMemory(getFreeMemory()));
|
||||
invocation.getSender().sendMessage("Used Memory: " + formatMemory(getUsedMemory()));
|
||||
invocation.getSender().sendMessage("Platform: " + SONAR.getServer().getPlatform().getDisplayName());
|
||||
invocation.getSender().sendMessage("Version: " + SONAR.getVersion().getFull());
|
||||
@Unmodifiable
|
||||
private @NotNull Map<String, Object> collectMappedInformation() {
|
||||
val mappings = new WeakHashMap<String, Object>();
|
||||
mappings.put("sonar", new Dump.Sonar(
|
||||
SONAR.getVersion().getFull(),
|
||||
SONAR.getPlatform(),
|
||||
SONAR.getVersion().isOnMainBranch()
|
||||
));
|
||||
mappings.put("runtime", new Dump.Runtime(
|
||||
getVirtualCores(),
|
||||
RUNTIME_MX_BEAN.getInputArguments(),
|
||||
RUNTIME_MX_BEAN.getVmName(),
|
||||
RUNTIME_MX_BEAN.getVmVendor(),
|
||||
RUNTIME_MX_BEAN.getVmVersion()
|
||||
));
|
||||
mappings.put("os", new Dump.OS(
|
||||
System.getProperty("os.name"),
|
||||
System.getProperty("os.arch"),
|
||||
System.getProperty("os.version")
|
||||
));
|
||||
mappings.put("memory", new Dump.Memory(
|
||||
formatMemory(getTotalMemory()),
|
||||
formatMemory(getMaxMemory()),
|
||||
formatMemory(getFreeMemory()),
|
||||
formatMemory(getUsedMemory())
|
||||
));
|
||||
return mappings;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final class Dump {
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static final class Sonar {
|
||||
private final String version;
|
||||
private final SonarPlatform platform;
|
||||
private final boolean isOnMainBranch;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static final class Runtime {
|
||||
private final int virtualCores;
|
||||
private final List<String> jvmArguments;
|
||||
private final String vmName;
|
||||
private final String vmVendor;
|
||||
private final String vmVersion;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static final class Memory {
|
||||
private final String total;
|
||||
private final String max;
|
||||
private final String free;
|
||||
private final String used;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static final class OS {
|
||||
private final String name;
|
||||
private final String arch;
|
||||
private final String version;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -24,20 +24,18 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "lockdown",
|
||||
description = "Lock the server down"
|
||||
description = "Prevent all players from joining"
|
||||
)
|
||||
public final class LockdownCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
SONAR.getConfig().LOCKDOWN_ENABLED = !SONAR.getConfig().LOCKDOWN_ENABLED;
|
||||
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", SONAR.getConfig().LOCKDOWN_ENABLED);
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
|
||||
SONAR.getConfig().getLockdown().setEnabled(newState);
|
||||
SONAR.getConfig().getGeneralConfig().set("lockdown.enabled", newState);
|
||||
SONAR.getConfig().getGeneralConfig().save();
|
||||
|
||||
invocation.getSender().sendMessage(
|
||||
SONAR.getConfig().LOCKDOWN_ENABLED
|
||||
? SONAR.getConfig().LOCKDOWN_ACTIVATED
|
||||
: SONAR.getConfig().LOCKDOWN_DEACTIVATED
|
||||
);
|
||||
invocation.getSender().sendMessage(newState
|
||||
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -21,24 +21,22 @@ 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;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "reload",
|
||||
description = "Reload the configuration"
|
||||
description = "Reload all configurations"
|
||||
)
|
||||
public final class ReloadCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().RELOADING);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
|
||||
SONAR.reload();
|
||||
|
||||
final long timeTaken = System.currentTimeMillis() - startTime;
|
||||
invocation.getSender().sendMessage(
|
||||
SONAR.getConfig().RELOADED
|
||||
.replace("%taken%", String.valueOf(timeTaken))
|
||||
);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
|
||||
.replace("%taken%", String.valueOf(timer.delay())));
|
||||
}
|
||||
}
|
||||
+60
-49
@@ -40,60 +40,71 @@ import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
argumentsRequired = false
|
||||
)
|
||||
public final class StatisticsCommand extends Subcommand implements JVMProfiler {
|
||||
private enum StatisticType {
|
||||
GENERAL,
|
||||
NETWORK,
|
||||
MEMORY,
|
||||
CPU
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String showing = invocation.getRawArguments().length >= 2
|
||||
? invocation.getRawArguments()[1].toLowerCase() : "bot";
|
||||
|
||||
invocation.getSender().sendMessage("§eShowing " + showing + " statistics for this session:");
|
||||
invocation.getSender().sendMessage("§ePlease note that session statistics are not saved when the server restarts.");
|
||||
invocation.getSender().sendMessage();
|
||||
|
||||
if (!showing.equals("bot")) {
|
||||
switch (showing) {
|
||||
case "network": {
|
||||
invocation.getSender().sendMessage(" §7▪ §aCurrent incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedSecond());
|
||||
invocation.getSender().sendMessage(" §7▪ §aCurrent outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedSecond());
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedTtl());
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedTtl());
|
||||
return;
|
||||
}
|
||||
|
||||
case "memory": {
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal free memory (JVM): §f" + formatMemory(getFreeMemory()));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal used memory (JVM): §f" + formatMemory(getUsedMemory()));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal maximum memory (JVM): §f" + formatMemory(getMaxMemory()));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal allocated memory (JVM): §f" + formatMemory(getTotalMemory()));
|
||||
return;
|
||||
}
|
||||
|
||||
case "cpu": {
|
||||
invocation.getSender().sendMessage(" §7▪ §aProcess CPU usage right now: §f" + DECIMAL_FORMAT.format(getProcessCPUUsage()) + "%");
|
||||
invocation.getSender().sendMessage(" §7▪ §aSystem CPU usage right now: §f" + DECIMAL_FORMAT.format(getSystemCPUUsage()) + "%");
|
||||
invocation.getSender().sendMessage(" §7▪ §aPer-core process CPU usage: §f" + DECIMAL_FORMAT.format(getAverageProcessCPUUsage()) + "%");
|
||||
invocation.getSender().sendMessage(" §7▪ §aPer-core system CPU usage: §f" + DECIMAL_FORMAT.format(getAverageSystemCPUUsage()) + "%");
|
||||
invocation.getSender().sendMessage(" §7▪ §aGeneral system load average: §f" + DECIMAL_FORMAT.format(getSystemLoadAverage()) + "%");
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal virtual cpu cores (JVM): §f" + DECIMAL_FORMAT.format(getVirtualCores()));
|
||||
return;
|
||||
}
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
StatisticType type = StatisticType.GENERAL;
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
try {
|
||||
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
|
||||
} catch (Exception exception) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
|
||||
.replace("%statistics%", getArguments()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final int total = Statistics.TOTAL_TRAFFIC.get();
|
||||
final int real = Statistics.REAL_TRAFFIC.get();
|
||||
final int failed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final int queued = SONAR.getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int verifying = SONAR.getFallback().getConnected().size();
|
||||
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
|
||||
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
|
||||
.replace("%type%", type.name().toLowerCase()));
|
||||
invocation.getSender().sendMessage();
|
||||
|
||||
invocation.getSender().sendMessage(" §7▪ §aVerified IP addresses: §f" + DECIMAL_FORMAT.format(verified));
|
||||
invocation.getSender().sendMessage(" §7▪ §aVerifying IP addresses: §f" + DECIMAL_FORMAT.format(verifying));
|
||||
invocation.getSender().sendMessage(" §7▪ §aBlacklisted IP addresses: §f" + DECIMAL_FORMAT.format(blacklisted));
|
||||
invocation.getSender().sendMessage(" §7▪ §aCurrently queued logins: §f" + DECIMAL_FORMAT.format(queued));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal non-unique joins: §f" + DECIMAL_FORMAT.format(total));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal verification attempts: §f" + DECIMAL_FORMAT.format(real));
|
||||
invocation.getSender().sendMessage(" §7▪ §aTotal failed verifications: §f" + DECIMAL_FORMAT.format(failed));
|
||||
switch (type) {
|
||||
case GENERAL: {
|
||||
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("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%total_attempts%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%total_failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get())));
|
||||
break;
|
||||
}
|
||||
|
||||
case CPU: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
|
||||
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
|
||||
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
|
||||
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
|
||||
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
|
||||
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
|
||||
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
|
||||
break;
|
||||
}
|
||||
|
||||
case MEMORY: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
|
||||
.replace("%free_memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%used_memory%", formatMemory(getUsedMemory()))
|
||||
.replace("%max_memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%total_memory%", formatMemory(getTotalMemory())));
|
||||
break;
|
||||
}
|
||||
|
||||
case NETWORK: {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
|
||||
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
|
||||
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
|
||||
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
|
||||
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-30
@@ -17,50 +17,30 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSender;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verbose",
|
||||
description = "Enable and disable Sonar verbose",
|
||||
description = "Enable or disable Sonar verbose",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class VerboseCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
InvocationSender verboseSubscriber = invocation.getSender();
|
||||
|
||||
// Support for '/sonar verbose [username]'
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
final Optional<InvocationSender> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
|
||||
if (optional.isPresent()) verboseSubscriber = optional.get();
|
||||
}
|
||||
|
||||
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
|
||||
if (verboseSubscriber != invocation.getSender()) {
|
||||
verboseSubscriber.sendMessage(
|
||||
SONAR.getConfig().VERBOSE_UNSUBSCRIBED_OTHER
|
||||
.replace("%player%", verboseSubscriber.getName())
|
||||
);
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_UNSUBSCRIBED);
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
// Reset ActionBar component when unsubscribing
|
||||
invocation.getSender().getAudience().sendActionBar(Component.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
if (verboseSubscriber != invocation.getSender()) {
|
||||
verboseSubscriber.sendMessage(
|
||||
SONAR.getConfig().VERBOSE_SUBSCRIBED_OTHER
|
||||
.replace("%player%", verboseSubscriber.getName())
|
||||
);
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_SUBSCRIBED);
|
||||
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
|
||||
}
|
||||
}
|
||||
+36
-40
@@ -24,6 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -39,75 +40,70 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
}
|
||||
)
|
||||
public final class VerifiedCommand extends Subcommand {
|
||||
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(10);
|
||||
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified history <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String inetAddress = invocation.getRawArguments()[2];
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
if (!IP_PATTERN.matcher(inetAddress).matches()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
|
||||
// Make sure the IP is verified already
|
||||
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need to parse this, so we can just use the string.
|
||||
final String realInetAddress = "/" + inetAddress;
|
||||
|
||||
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
invocation.getSender().sendMessage("§ePrevious UUIDs for " + inetAddress + ":");
|
||||
|
||||
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
|
||||
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
|
||||
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
|
||||
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "remove": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified remove <IP address>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String inetAddress = invocation.getRawArguments()[2];
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
|
||||
if (!IP_PATTERN.matcher(inetAddress).matches()) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
|
||||
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
|
||||
if (LOCK.contains(rawInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't need to parse this, so we can just use the string.
|
||||
final String realInetAddress = "/" + inetAddress;
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
if (LOCK.contains(realInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_BLOCKED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
|
||||
// Make sure the player is verified already
|
||||
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the IP address
|
||||
// Make sure we don't accidentally run 2 operations at the same time
|
||||
LOCK.add(realInetAddress);
|
||||
SONAR.getVerifiedPlayerController().remove(realInetAddress);
|
||||
LOCK.remove(realInetAddress);
|
||||
LOCK.add(rawInetAddress);
|
||||
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_REMOVE
|
||||
.replace("%ip%", inetAddress));
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
// Unlock the IP address
|
||||
LOCK.remove(rawInetAddress);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -115,19 +111,19 @@ public final class VerifiedCommand extends Subcommand {
|
||||
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
|
||||
|
||||
if (verified == 0) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_EMPTY);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate all cache entries
|
||||
SONAR.getVerifiedPlayerController().clearAll();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_CLEARED
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
|
||||
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
|
||||
break;
|
||||
}
|
||||
|
||||
case "size": {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_SIZE
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
|
||||
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package xyz.jonesdev.sonar.common.update;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@UtilityClass
|
||||
public class UpdateChecker {
|
||||
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final LoggerWrapper LOGGER = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().info("[update-checker] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().warn("[update-checker] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
Sonar.get().getLogger().error("[update-checker] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
|
||||
final String[] convertedParts = version.split("\\.");
|
||||
// Validate the length of the converted parts
|
||||
if (convertedParts.length != 3) {
|
||||
throw new IllegalStateException("Converted version length mismatch");
|
||||
}
|
||||
// Major updates are more important than minor updates,
|
||||
// and minor updates are more important than patches.
|
||||
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
|
||||
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
|
||||
final int patch = Integer.parseInt(convertedParts[2]);
|
||||
return convertedMajor + convertedMinor + patch; // sum the converted version parts
|
||||
}
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
|
||||
final HttpsURLConnection urlConnection = prepareConnection(url);
|
||||
final JsonObject json = parseJson(urlConnection);
|
||||
final String latestStableRelease = json.get("tag_name").getAsString();
|
||||
final int convertedLatestVersion = convertVersion(latestStableRelease);
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
|
||||
|
||||
if (convertedCurrentVersion < convertedLatestVersion) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
|
||||
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
|
||||
LOGGER.warn("You are currently using an unreleased version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
} else {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Could not retrieve latest version information: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
|
||||
try (final InputStream inputStream = urlConnection.getInputStream()) {
|
||||
try (final InputStreamReader inputStreamReader = new InputStreamReader(inputStream)) {
|
||||
final StringBuilder content = new StringBuilder();
|
||||
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
urlConnection.setReadTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestMethod("GET");
|
||||
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
|
||||
// Only continue if the connection is a success
|
||||
if (urlConnection.getResponseCode() != 200) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
return urlConnection;
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.common.utility.geyser;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -27,20 +28,20 @@ import org.jetbrains.annotations.NotNull;
|
||||
@UtilityClass
|
||||
public class GeyserUtil {
|
||||
|
||||
/*
|
||||
* Geyser attribute key for every floodgate player
|
||||
*/
|
||||
private AttributeKey<Object> playerAttribute() {
|
||||
return AttributeKey.valueOf("floodgate-player");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Geyser holds the parent channel of the player
|
||||
*
|
||||
* @param channel Channel of the player
|
||||
* @return Whether the player is on GeyserMC or not
|
||||
*/
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel) {
|
||||
// Get the parent channel of the original channel
|
||||
final Channel parent = channel.parent();
|
||||
// This shouldn't happen, but we want to stay safe here
|
||||
if (parent == null) return false;
|
||||
|
||||
final Class<? extends Channel> clazz = parent.getClass();
|
||||
// Check if Geyser adapted the channel by checking for the package name
|
||||
return clazz.getCanonicalName().startsWith("org.geysermc");
|
||||
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
|
||||
// Every floodgate player has a channel attribute called "floodgate-player"
|
||||
return channel.hasAttr(playerAttribute());
|
||||
}
|
||||
}
|
||||
+24
-22
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
@@ -33,7 +34,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// Mostly taken from
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
public class ProtocolUtil {
|
||||
@@ -47,40 +48,41 @@ public class ProtocolUtil {
|
||||
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
|
||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
||||
|
||||
public static @NotNull String readString(final ByteBuf buf) throws CorruptedFrameException {
|
||||
return readString(buf, Short.MAX_VALUE);
|
||||
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
|
||||
final ByteBuf slice = content.slice();
|
||||
try {
|
||||
return readString(slice);
|
||||
} catch (DecoderException exception) {
|
||||
return readString(slice, 65536, slice.readableBytes());
|
||||
}
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf buf,
|
||||
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
|
||||
return readString(byteBuf, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf byteBuf,
|
||||
final int cap) throws CorruptedFrameException {
|
||||
return readString(buf, cap, false);
|
||||
final int length = VarIntUtil.readVarInt(byteBuf);
|
||||
return readString(byteBuf, cap, length);
|
||||
}
|
||||
|
||||
public static @NotNull String readString(final ByteBuf buf,
|
||||
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
|
||||
final int cap,
|
||||
final boolean legacy) throws CorruptedFrameException {
|
||||
final int length = VarIntUtil.readVarInt(buf);
|
||||
|
||||
final int length) throws CorruptedFrameException {
|
||||
checkFrame(length >= 0, "Got a negative-length string");
|
||||
checkFrame(length <= cap * 3, "Bad string size");
|
||||
String str;
|
||||
if (legacy) {
|
||||
// TODO: length checking?
|
||||
str = buf.toString(StandardCharsets.UTF_8);
|
||||
buf.skipBytes(buf.readableBytes());
|
||||
} else {
|
||||
checkFrame(buf.isReadable(length), "Got an invalid string length");
|
||||
str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
|
||||
buf.skipBytes(length);
|
||||
}
|
||||
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
|
||||
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
|
||||
byteBuf.skipBytes(length);
|
||||
checkFrame(str.length() <= cap, "Got a too-long string");
|
||||
return str;
|
||||
}
|
||||
|
||||
public static void writeString(final ByteBuf buf, final CharSequence str) {
|
||||
public static void writeString(final ByteBuf byteBuf, final CharSequence str) {
|
||||
final int size = ByteBufUtil.utf8Bytes(str);
|
||||
VarIntUtil.writeVarInt(buf, size);
|
||||
buf.writeCharSequence(str, StandardCharsets.UTF_8);
|
||||
VarIntUtil.writeVarInt(byteBuf, size);
|
||||
byteBuf.writeCharSequence(str, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void writeUUID(final @NotNull ByteBuf byteBuf, final @NotNull UUID uuid) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File renamed without changes.
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user