Compare commits

...
27 Commits
Author SHA1 Message Date
Jones 18bb9557b1 Release 2.0.16 (#183) 2024-01-28 15:00:42 +01:00
Jones 5d02050815 Load Sonar after some other plugins for compatibility (#182) 2024-01-26 15:06:29 +01:00
Jones 5914b23ae5 Fix relocations breaking dynamic library loading (#180) 2024-01-26 14:58:23 +01:00
Jones 7ce56a79ab Remove unused relocations (#178) 2024-01-25 21:39:51 +01:00
Jones 7b17ba7e53 Bump adventure-platform to 4.3.2 (#177) 2024-01-25 20:24:10 +01:00
AkkariinandMichel Elkenwaat 8268cda660 Recode packet handling on BungeeCord (#174)
* feat: switch to HandlerBoss instead of using InitialHandler.

* feat: recode channel wrapper initialization structure

* fix: remove unnecessary/duplicate methods

* fix: update initial BungeeCord methods (check upstream)

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-01-25 19:28:37 +01:00
Jones 89d0c7346f Use scheduler instead of custom threads for services (#176)
* fix(bungee): kyori title being broken on BungeeCord

* fix(all): use server's scheduler instead of own services

* fix(velocity): tasks not being scheduled correctly

* fix(velocity): too-many-online-per-IP check breaking on 1.20.2+
2024-01-23 19:36:59 +01:00
Jones 512faa956e Fix protocol check not using the correct disconnect method (#168)
Related to https://github.com/jonesdevelopment/sonar/issues/167
2024-01-22 21:12:02 +01:00
Jones 1f6781aaa3 Load all libraries dynamically during runtime (#166)
* feat: use libby for dynamic dependency loading

* fix: MySQL driver not working with new library loading

* style: remove debug messages

* style: make setup and invoke void static
2024-01-22 20:35:22 +01:00
Jones bf40333de5 Fix incompatibility with LibreLogin on BungeeCord (#164) 2024-01-22 14:41:23 +01:00
dependabot[bot] 31b961b577 Bump netty-all from 4.1.104.Final to 4.1.106.Final (#162)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.104.Final to 4.1.106.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.104.Final...netty-4.1.106.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 13:52:22 +01:00
Jones e4821781fa Make /sonar notify only executable by players (#161) 2024-01-21 15:05:23 +01:00
Jones bdcb7d28a7 Fix broken disconnect packets during login on 1.20.2+ (#160) 2024-01-21 14:51:05 +01:00
Jones 433fab10f1 Implement chat and title attack notifications (#158)
* feat: move audiences outside the verbose handler

* build(deps): only relocate audience class instead of package

* feat: implement chat and title notifications

* feat: allow chat notifications to contain traffic
2024-01-21 14:44:52 +01:00
Jones 014b956f9e Fix compiler errors since Velocity 3.3.0-329 (#157) 2024-01-20 11:42:27 +01:00
Jones 4be1a970d0 Remove some duplicate code in the API (#156)
* fix(api): remove duplicate code in FallbackUser

* style(api): schedule FallbackUser#disconnect(String) for removal
2024-01-16 17:19:19 +01:00
Jones bea1d292ee Release 2.0.15 (#154) 2024-01-11 18:30:20 +01:00
Jones 468a11e0be Fix Geyser profiles by reverting parts of the new injection (#153)
* fix: check for Geyser by using the port instead of channel attribute

* feat: revert some of https://github.com/jonesdevelopment/sonar/commit/a66f60f1773ab517ec786ba264313f0d0ad7ed76 to fix https://github.com/jonesdevelopment/sonar/issues/152
2024-01-11 18:21:38 +01:00
Jones cf2fce9c29 Run DB cleaning and caching in the same thread (#151) 2024-01-07 17:22:35 +01:00
Jones cc52289500 Release 2.0.14 (#149) 2024-01-06 14:55:30 +01:00
Jones ac1f6d616b Reword and simplify the README (#148) 2024-01-02 21:26:04 +01:00
Jones bdd941171e Use correct security contact email (#147) 2024-01-02 21:09:25 +01:00
Jones 1e18a29d1e Update license year to 2023-2024 (#146) 2024-01-02 18:17:20 +01:00
Jones 5671c2029e Change default color scheme in messages (#145) 2024-01-01 23:55:33 +01:00
Jones 32aeee4f06 Fix license link in README (#144) 2023-12-31 18:10:28 +01:00
Jones 7b5ce41420 Show server uptime in statistics (#143)
* style: small code clean up

* feat: add server uptime to statistics
2023-12-30 21:44:16 +01:00
Jones a66f60f177 Recode Velocity injection (#142)
* feat(bungee): implement empty hostname check

* feat: recode Velocity injection

* feat: recode action bar counters

* fix(bungee): make sure the connection is open
2023-12-30 21:24:53 +01:00
143 changed files with 1519 additions and 1587 deletions

No files matched your search

+14 -23
View File
@@ -25,7 +25,7 @@
|
[Discord](https://jonesdev.xyz/discord)
|
[License](https://github.com/jonesdevelopment/sonar/blob/main/README.md#license)
[License](https://github.com/jonesdevelopment/sonar/?tab=readme-ov-file#license)
</div>
## Design and Goal
@@ -33,27 +33,25 @@
* Effective, lightweight, and easy-to-use
* No unnecessary features and clean code
* Protection against all kinds of bot attacks
* No player should be annoyed by any sort of captcha
* No player should be annoyed by any sort of [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)
* No sort of checking for VPNs or proxies
* Multi-platform support (Velocity, BungeeCord, and Bukkit)
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
## Checks
- New player logins are queued to prevent spam login attacks.
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
all types of bots.
- Spigot & BungeeCord
- Duplicate login/status packet spam fix
- Run server ping handling asynchronously
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
- Sonar queues new player logins to prevent spam login attacks
- Protective components only on BungeeCord:
- Sonar checks the handshake packets for legitimacy
- Sonar validates the login/server ping packets
- Sonar handles all server pings asynchronously
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
### Fallback
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever
reaching the backend.
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and
advanced types of bots.
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend.
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and advanced types of bots.
* Sends the player to a lightweight fake server when they connect for the first time.
* Analyzes if the player is sending the necessary packets.
@@ -61,16 +59,11 @@ advanced types of bots.
* Checks if the player is obeying client gravity.
* Checks if the player is colliding with blocks correctly.
Fallback also protects from huge spambot attacks since it queues the incoming connections, therefore making it
technically impossible to have a ton of bots join the server at the same time.
Fallback also protects against huge spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
### False positives
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. For example, some higher Minecraft versions have a bug where the client sometimes sends a packet out of order. Sonar accounts for that and does not falsely blacklist clients.
If you or one of your players experiences a false positive, make sure to report it by opening
a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket
on [Discord](https://jonesdev.xyz/discord/).
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check. However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket on [Discord](https://jonesdev.xyz/discord/).
## Building
@@ -80,9 +73,7 @@ You can also take a look at the [gradle documentation](https://docs.gradle.org/c
## Contributing
If you are interested in contributing, you can check out
the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed
instructions.
If you are interested in contributing, you can check out the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed instructions.
## License
+3 -7
View File
@@ -13,13 +13,9 @@ If you discover a security vulnerability within Sonar, please follow these steps
## Contact
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at admin@jonesdev.xyz.
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at contact@jonesdev.xyz.
## Acknowledgments
Special thanks to the following people for their contributions:
- [xism4](https://github.com/xism4)
- fixed serious issues with packet decode logic on BungeeCord [#5](https://github.com/jonesdevelopment/sonar-legacy/pull/5) (Severity: medium)
- fixed a false blacklisting issue when a player looses connection [#9](https://github.com/jonesdevelopment/sonar-legacy/pull/9) (Severity: low)
- [NoGodHenry](https://github.com/NoGodHenry)
- fixed a ConcurrentModificationException in the configuration [#3](https://github.com/jonesdevelopment/sonar-legacy/pull/3) (Severity: low)
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors),
and also those who reported security issues or bugs.
+18 -9
View File
@@ -21,8 +21,9 @@ versioner {
allprojects {
repositories {
mavenCentral() // Lombok
maven(url = "https://jitpack.io") // simple-yaml
maven(url = "https://jitpack.io/") // simple-yaml
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
}
apply(plugin = "java")
@@ -43,10 +44,13 @@ allprojects {
// adventure nbt
implementation("net.kyori:adventure-nbt:$adventureVersion")
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.104.Final") // netty
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
// Library/dependency loading
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
}
}
@@ -80,17 +84,22 @@ tasks {
// https://github.com/jonesdevelopment/sonar/issues/46
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
// We have to be careful here, so we don't accidentally break adventure
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
exclude("net.kyori.adventure.text.**")
exclude("net.kyori.adventure.audience.**")
exclude("net.kyori.adventure.audience.Audience")
exclude("net.kyori.adventure.title.*")
}
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
// Relocate dynamically loaded libraries
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
// We want to load our own Gson dynamically
exclude("com/google/gson/**")
// Exclude unnecessary metadata information
exclude("META-INF/versions/**")
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.13
version=2.0.16
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,5 +1,5 @@
dependencies {
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
exclude(group = "org.yaml")
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,8 +17,10 @@
package xyz.jonesdev.sonar.api;
import com.alessiodp.libby.LibraryManager;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
@@ -27,10 +29,14 @@ 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.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
@@ -40,6 +46,12 @@ public interface Sonar {
.disableInnerClassSerialization()
.create();
/**
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit
*/
Map<String, Audience> AUDIENCES = new ConcurrentHashMap<>();
/**
* @return The platform the plugin is being run on
*/
@@ -60,12 +72,24 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
@NotNull Notification getNotificationHandler();
@NotNull SystemTimer getLaunchTimer();
@NotNull LibraryManager getLibraryManager();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Set a custom notification handler
*/
@SuppressWarnings("unused")
void setNotificationHandler(final @NotNull Notification notificationHandler);
/**
* Reloads the entire plugin
*/
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,6 +24,7 @@ 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.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@@ -43,6 +44,7 @@ public final class AttackTracker implements JVMProfiler {
private final SystemTimer duration = new SystemTimer();
private final SystemTimer timer = new SystemTimer();
private int peakJoinsPerSecond;
private int peakConnectionsPerSecond;
private double peakProcessCPUUsage;
private long peakProcessMemoryUsage;
// Calculate during-attack-statistics using their deltas
@@ -50,7 +52,7 @@ public final class AttackTracker implements JVMProfiler {
}
public void checkIfUnderAttack() {
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().estimatedSize();
final int joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
@@ -67,6 +69,7 @@ public final class AttackTracker implements JVMProfiler {
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().sendAttackNotification();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
@@ -77,6 +80,11 @@ public final class AttackTracker implements JVMProfiler {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final int connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
}
final double processCPUUsage = getProcessCPUUsage();
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
// Update cpu usage peak if necessary
@@ -99,6 +107,7 @@ public final class AttackTracker implements JVMProfiler {
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
@@ -121,6 +130,7 @@ public final class AttackTracker implements JVMProfiler {
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,7 +24,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.dependencies.Dependency;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.awt.*;
@@ -53,6 +52,8 @@ public final class SonarConfiguration {
private final Database database = new Database();
@Getter
private final Webhook webhook = new Webhook();
@Getter
private final Notifications notifications = new Notifications();
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
@@ -86,6 +87,14 @@ public final class SonarConfiguration {
private List<String> animation;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Notifications {
private Component notificationTitle;
private Component notificationSubtitle;
private String notificationChat;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Queue {
@@ -219,6 +228,9 @@ public final class SonarConfiguration {
private String verboseSubscribed;
private String verboseUnsubscribed;
private String notificationsSubscribed;
private String notificationsUnsubscribed;
private String reloading;
private String reloaded;
}
@@ -230,10 +242,9 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL(Dependency.MYSQL, "com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null, null);
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null);
private final Dependency dependency;
private final String driverClassName;
}
@@ -340,7 +351,8 @@ public final class SonarConfiguration {
messagesConfig.getYaml().setComment("prefix",
"Placeholder for every '%prefix%' in this configuration file");
prefix = formatString(messagesConfig.getString("prefix", "<yellow><bold>Sonar<reset> <gray>» <white>"));
prefix = formatString(messagesConfig.getString("prefix",
"<b><yellow>Sonar</b> <gray>» <white>"));
messagesConfig.getYaml().setComment("support-url",
"Placeholder for every '%support-url%' in this configuration file");
@@ -641,8 +653,8 @@ public final class SonarConfiguration {
"Placeholder for every '%header%' in this configuration file");
header = fromList(messagesConfig.getStringList("header",
Arrays.asList(
"<yellow><bold>Sonar<reset>",
"<reset>"
"<b><yellow>Sonar</b><reset>",
""
)));
messagesConfig.getYaml().setComment("footer",
@@ -708,15 +720,15 @@ public final class SonarConfiguration {
"Informational message that is shown above everything when running the main command");
commands.helpHeader = messagesConfig.getStringList("commands.main.header",
Arrays.asList(
"<yellow>Running Sonar %version% on %platform%.",
"<yellow>(C) %copyright_year% Jones Development and Sonar Contributors",
"<green><click:open_url:'https://github.com/jonesdevelopment/sonar'>https://github.com/jonesdevelopment/sonar",
"<yellow>Running <b>Sonar</b> %version% on %platform%.",
"<gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year% Jones Development and" +
" Sonar Contributors</click>",
"",
"<yellow>Need help or have any questions?",
"<yellow><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a " +
"ticket on the Discord </hover></click></yellow><yellow><click:open_url:'https://github" +
".com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on " +
"GitHub.",
"<gray>Need help or have any questions?",
"<white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a " +
"ticket on the Discord </hover></click><click:open_url:'https://github" +
".com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on" +
" GitHub.",
""
));
messagesConfig.getYaml().setComment("commands.main.subcommands",
@@ -724,8 +736,8 @@ public final class SonarConfiguration {
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands",
"<click:suggest_command:'/sonar %subcommand% '><hover:show_text:'<gray>Only players: " +
"</gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission: " +
"</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%'><gray> ▪ </gray><green>/sonar " +
"%subcommand%</green><gray> - </gray><white>%description%"));
"</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%'><dark_aqua> ▪ <gray>/sonar " +
"%subcommand% <white>%description%"));
SonarCommand.prepareCachedMessages();
@@ -753,6 +765,18 @@ public final class SonarConfiguration {
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed",
"%prefix%You are no longer viewing Sonar verbose."));
messagesConfig.getYaml().setComment("commands.notify",
"Translations for '/sonar notify'");
messagesConfig.getYaml().setComment("commands.notify.subscribed",
"Message that is shown when a player subscribes to Sonar attack notifications");
commands.notificationsSubscribed = formatString(messagesConfig.getString("commands.notify.subscribed",
"%prefix%You are now viewing Sonar attack notifications."));
messagesConfig.getYaml().setComment("commands.notify.unsubscribed",
"Message that is shown when a player unsubscribes from Sonar attack notifications");
commands.notificationsUnsubscribed = formatString(messagesConfig.getString("commands.notify.unsubscribed",
"%prefix%You are no longer viewing Sonar attack notifications."));
messagesConfig.getYaml().setComment("commands.blacklist",
"Translations for '/sonar blacklist'");
messagesConfig.getYaml().setComment("commands.blacklist.empty",
@@ -833,7 +857,7 @@ public final class SonarConfiguration {
messagesConfig.getYaml().setComment("commands.statistics.header",
"Informational message that is shown above everything when viewing the statistics");
commands.statisticsHeader = formatString(messagesConfig.getString("commands.statistics.header",
"%prefix%<yellow>Showing %type% statistics for this session:"));
"%prefix%Showing %type% statistics for this session:"));
messagesConfig.getYaml().setComment("commands.statistics.unknown-type",
"Message that is shown when a player tries viewing an unknown statistic");
@@ -844,45 +868,46 @@ public final class SonarConfiguration {
"Format of the general statistics message");
commands.generalStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.general",
Arrays.asList(
" <gray>▪ <green>Verified IP addresses: <white>%verified%",
" <gray>▪ <green>Verifying IP addresses: <white>%verifying%",
" <gray>▪ <green>Blacklisted IP addresses: <white>%blacklisted%",
" <gray>▪ <green>Currently queued logins: <white>%queued%",
" <gray>▪ <green>Total non-unique joins: <white>%total_joins%",
" <gray>▪ <green>Total verification attempts: <white>%total_attempts%",
" <gray>▪ <green>Total failed verifications: <white>%total_failed%"
" <dark_aqua>▪ <gray>Verified IP addresses: <white>%verified%",
" <dark_aqua>▪ <gray>Verifying IP addresses: <white>%verifying%",
" <dark_aqua>▪ <gray>Blacklisted IP addresses: <white>%blacklisted%",
" <dark_aqua>▪ <gray>Currently queued logins: <white>%queued%",
" <dark_aqua>▪ <gray>Total non-unique joins: <white>%total_joins%",
" <dark_aqua>▪ <gray>Total verification attempts: <white>%total_attempts%",
" <dark_aqua>▪ <gray>Total failed verifications: <white>%total_failed%",
" <dark_aqua>▪ <gray>Server uptime: <white>%uptime%"
))));
messagesConfig.getYaml().setComment("commands.statistics.cpu",
"Format of the CPU statistics message");
commands.cpuStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.cpu",
Arrays.asList(
" <gray>▪ <green>Process CPU usage right now: <white>%process_cpu%%",
" <gray>▪ <green>System CPU usage right now: <white>%system_cpu%%",
" <gray>▪ <green>Per-core process CPU usage: <white>%average_process_cpu%%",
" <gray>▪ <green>Per-core system CPU usage: <white>%average_system_cpu%%",
" <gray>▪ <green>General system load average: <white>%load_average%%",
" <gray>▪ <green>Total amount of virtual cpus: <white>%virtual_cores%"
" <dark_aqua>▪ <gray>Process CPU usage right now: <white>%process_cpu%%",
" <dark_aqua>▪ <gray>System CPU usage right now: <white>%system_cpu%%",
" <dark_aqua>▪ <gray>Per-core process CPU usage: <white>%average_process_cpu%%",
" <dark_aqua>▪ <gray>Per-core system CPU usage: <white>%average_system_cpu%%",
" <dark_aqua>▪ <gray>General system load average: <white>%load_average%%",
" <dark_aqua>▪ <gray>Total amount of virtual cpus: <white>%virtual_cores%"
))));
messagesConfig.getYaml().setComment("commands.statistics.memory",
"Format of the memory statistics message");
commands.memoryStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.memory",
Arrays.asList(
" <gray>▪ <green>Total free memory: <white>%free_memory%",
" <gray>▪ <green>Total used memory: <white>%used_memory%",
" <gray>▪ <green>Total maximum memory: <white>%max_memory%",
" <gray>▪ <green>Total allocated memory: <white>%total_memory%"
" <dark_aqua>▪ <gray>Total free memory: <white>%free_memory%",
" <dark_aqua>▪ <gray>Total used memory: <white>%used_memory%",
" <dark_aqua>▪ <gray>Total maximum memory: <white>%max_memory%",
" <dark_aqua>▪ <gray>Total allocated memory: <white>%total_memory%"
))));
messagesConfig.getYaml().setComment("commands.statistics.network",
"Format of the network statistics message");
commands.networkStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.network",
Arrays.asList(
" <gray>▪ <green>Current incoming used bandwidth: <white>%incoming%",
" <gray>▪ <green>Current outgoing used bandwidth: <white>%outgoing%",
" <gray>▪ <green>Total incoming used bandwidth: <white>%ttl_incoming%",
" <gray>▪ <green>Total outgoing used bandwidth: <white>%ttl_outgoing%"
" <dark_aqua>▪ <gray>Current incoming used bandwidth: <white>%incoming%",
" <dark_aqua>▪ <gray>Current outgoing used bandwidth: <white>%outgoing%",
" <dark_aqua>▪ <gray>Total incoming used bandwidth: <white>%ttl_incoming%",
" <dark_aqua>▪ <gray>Total outgoing used bandwidth: <white>%ttl_outgoing%"
))));
messagesConfig.getYaml().setComment("verification",
@@ -1051,9 +1076,9 @@ public final class SonarConfiguration {
))));
messagesConfig.getYaml().setComment("verbose",
"Translations for all messages regarding Sonar's verbose output");
"Translations for Sonar's actionbar verbose messages");
messagesConfig.getYaml().setComment("verbose.layout.normal",
"General layout for the verbose action-bar"
"Layout for Sonar's actionbar verbose"
+ LINE_SEPARATOR + "Placeholders:"
+ LINE_SEPARATOR + "- '%queued%' Number of queued connections"
+ LINE_SEPARATOR + "- '%verifying%' Number of verifying connections"
@@ -1063,6 +1088,7 @@ public final class SonarConfiguration {
+ LINE_SEPARATOR + "- '%verify-success%' Number of verified IP addresses"
+ LINE_SEPARATOR + "- '%verify-failed%' Number of failed verifications"
+ LINE_SEPARATOR + "- '%logins-per-second%' Number of logins per second"
+ LINE_SEPARATOR + "- '%connections-per-second%' Number of connections per second"
+ LINE_SEPARATOR + "- '%attack-duration%' Duration of the current attack"
+ LINE_SEPARATOR + "- '%incoming-traffic%' Incoming bandwidth usage per second"
+ LINE_SEPARATOR + "- '%outgoing-traffic%' Outgoing bandwidth usage per second"
@@ -1076,24 +1102,22 @@ public final class SonarConfiguration {
+ LINE_SEPARATOR + "Translations for Sonar's normal verbose output");
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal",
String.join(" <dark_aqua>╺ ", Arrays.asList(
"%prefix%<gray>Queued <white>%queued%",
"<gray>Verifying <white>%verifying%",
"<gray>Blacklisted <white>%blacklisted%" +
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
" <green><bold>%animation%<reset>"
))));
"%prefix%<gray>CPS <white>%connections-per-second%",
"<gray>Logins/s <white>%logins-per-second%",
"<gray>Verifying <white>%verifying%"
+ " <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s"
+ " <green><bold>%animation%<reset>"))));
messagesConfig.getYaml().setComment("verbose.layout.attack",
"Translations for Sonar's verbose output during an active attack");
"Layout for Sonar's actionbar verbose during an attack");
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack",
String.join(" <dark_aqua>╺ ", Arrays.asList(
"%prefix%<gray>BPS <white>%logins-per-second%",
"<gray>Queued <white>%queued%",
"%prefix%<gray>CPS <white>%connections-per-second%",
"<gray>Logins/s <white>%logins-per-second%",
"<gray>Queue <white>%queued%",
"<gray>Verifying <white>%verifying%",
"<gray>Blacklisted <white>%blacklisted%",
"<gray>Duration <white>%attack-duration%" +
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
" <green><bold>%animation%<reset>"
))));
"<gray>Duration <white>%attack-duration%"
+ " <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s"
+ " <green><bold>%animation%<reset>"))));
messagesConfig.getYaml().setComment("verbose.animation", "Animation for the action bar"
+ LINE_SEPARATOR + "Alternatives:"
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
@@ -1102,6 +1126,39 @@ public final class SonarConfiguration {
Arrays.asList("◜", "◝", "◞", "◟")
));
messagesConfig.getYaml().setComment("notifications",
"Translations for Sonar's attack notification messages");
messagesConfig.getYaml().setComment("notifications.title",
"Layout for Sonar's attack notification title");
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title",
"<yellow><b>Sonar<reset>")));
messagesConfig.getYaml().setComment("notifications.subtitle",
"Layout for Sonar's attack notification subtitle");
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle",
"<gray>A bot attack has been detected")));
messagesConfig.getYaml().setComment("notifications.chat",
"Layout for Sonar's attack notification in chat");
notifications.notificationChat = formatString(fromList(
messagesConfig.getStringList("notifications.chat",
Arrays.asList(
"",
"<yellow><b>Sonar<reset>",
"",
"<gray>A bot attack has been detected.",
" <dark_aqua>▪ <gray>Logins per second: <white>%logins-per-second%",
" <dark_aqua>▪ <gray>Verifying players: <white>%verifying%",
" <dark_aqua>▪ <gray>Queued players: <white>%queued%",
"",
"<gray>View more information using " +
"<white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click>" +
"<gray> or " +
"<white><click:run_command:'/sonar stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click>" +
"<gray>.",
//"<gray>If you have any questions regarding attack mitigation or experience issues,",
//"<gray>you can join the Jones Development Discord server or open a GitHub issue.",
""
))));
generalConfig.save();
messagesConfig.save();
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.controller;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -28,12 +29,11 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.dependencies.DependencyLoader;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -61,7 +61,7 @@ public final class VerifiedPlayerController {
return;
}
try (final ConnectionSource connectionSource = DependencyLoader.setUpDriverAndConnect()) {
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
this.connectionSource = connectionSource;
// Create table
@@ -76,16 +76,89 @@ public final class VerifiedPlayerController {
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
queryBuilder = dao.queryBuilder();
// Make sure to clear all outdated entries first
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
DB_UPDATE_SERVICE.execute(() -> {
// Make sure to clear all outdated entries first
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
// Add all entries from the database to the cache
try {
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
Sonar.get().getLogger().error("Could not cache entries: {}", exception);
}
});
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
Sonar.get().getLogger().error("Error setting up database: {}", throwable);
throwable.printStackTrace(System.err);
}
}
/**
* Automatically sets up the database connection using the configured credentials
*/
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Prepare the JDBC connection string
// TODO: automatically build best connection string for database type
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
// Use reflection to uncover the driver class
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
// Make sure we send the username and password
final Properties credentials = new Properties();
credentials.put("user", database.getUsername());
credentials.put("password", database.getPassword());
// Return new JDBC connection instance for our controller to handle
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
}
/**
* Uses reflection to load the database driver from the injected library
*
* @param connectionString URL of the database
* @param driverClass Class of the database driver
* @param credentials Credentials for the database
*/
private static Connection invoke(final @NotNull String connectionString,
final @NotNull Class<?> driverClass,
final @NotNull Properties credentials) throws Throwable {
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, connectionString, credentials);
}
/**
* Clear all old entries using the given timestamp.
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
try {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
}
}
/**
* First, remove the player from the local cache and then,
* secondly, asynchronously add the player to the database.
@@ -204,44 +277,6 @@ public final class VerifiedPlayerController {
}
}
/**
* Clear all old entries using the given timestamp.
*/
public void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
return;
}
try {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to clear old entries: {}", exception);
}
});
}
/**
* @param inetAddress IP address
* @param uuid UUID associated to the IP
@@ -1,90 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
public enum Dependency {
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
MYSQL(
"com/mysql",
"mysql-connector-j",
"8.2.0"
),
// TODO: MariaDB still seems to have some issues; investigate!
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
MARIADB(
"org/mariadb/jdbc",
"mariadb-java-client",
"3.1.4"
);
private final Path tempFilePath;
private final URL mvnRepoURL;
Dependency(final @NotNull String groupId,
final @NotNull String artifactId,
final @NotNull String version) {
final String archiveFileName = String.format("%s-%s.jar", artifactId, version);
this.tempFilePath = new File(Sonar.get().getDataDirectory(), "libraries/" + archiveFileName).toPath();
try {
final String mvnCentralPath = String.format("%s/%s/%s/%s-%s.jar",
groupId, artifactId, version, artifactId, version);
this.mvnRepoURL = new URL("https://repo1.maven.org/maven2/" + mvnCentralPath);
} catch (MalformedURLException exception) {
throw new IllegalStateException(exception);
}
}
/**
* @return URL for the local file used for the external URLClassLoader
*/
public @NotNull URL getClassLoaderURL() throws MalformedURLException {
if (!Files.exists(tempFilePath)) {
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Downloading dependency {}...", tempFilePath.getFileName());
try (final InputStream inputStream = mvnRepoURL.openStream()) {
Files.createDirectories(tempFilePath.getParent());
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
} catch (IOException exception) {
throw new IllegalStateException(exception);
}
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
}
return tempFilePath.toUri().toURL();
}
}
@@ -1,81 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
import com.j256.ormlite.support.ConnectionSource;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Properties;
@UtilityClass
public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
ClassLoader classLoader = currentClassLoader;
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
final URL url = database.getType().getDependency().getClassLoaderURL();
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
addPath.setAccessible(true);
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
classLoader = new ExternalClassLoader(new URL[]{url});
}
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
final Connection connection = connect(classLoader, jdbcURL, database);
return new JdbcSingleConnectionSource(jdbcURL, connection);
}
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration.Database database) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
properties.put("user", database.getUsername());
properties.put("password", database.getPassword());
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, databaseURL, properties);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,5 +31,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser<?, ?> user;
private final FallbackUser<?> user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,11 +30,11 @@ import java.util.Optional;
@ToString
@EqualsAndHashCode
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?, ?> user;
private final FallbackUser<?> user;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<String> reason;
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
public UserVerifyFailedEvent(final FallbackUser<?> user, final @Nullable String reason) {
this.user = user;
this.reason = Optional.ofNullable(reason);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,5 +30,5 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser<?, ?> user;
private final FallbackUser<?> user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,6 +33,6 @@ import java.util.UUID;
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser<?, ?> user;
private final FallbackUser<?> user;
private final long timeTakenToVerify;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,25 +31,10 @@ import java.util.concurrent.ConcurrentHashMap;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class FallbackQueue {
public static final FallbackQueue INSTANCE = new FallbackQueue();
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(16, 0.5f);
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
/**
* @param inetAddress IP address of the player
* @param runnable Queued action on the netty thread
* @see #remove
*/
public void queue(final InetAddress inetAddress, final Runnable runnable) {
queuedPlayers.put(inetAddress, runnable);
}
/**
* @param inetAddress IP address of the player
*/
public void remove(final InetAddress inetAddress) {
queuedPlayers.remove(inetAddress);
}
public void poll() {
// Runnable task executed for polling the queue
private final Runnable pollTask = () -> {
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
@@ -71,5 +56,21 @@ public final class FallbackQueue {
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Clean up the cache of rate-limited IPs
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
};
/**
* @param inetAddress IP address of the player
* @param runnable Queued action on the netty thread
* @see #remove
*/
public void queue(final InetAddress inetAddress, final Runnable runnable) {
queuedPlayers.put(inetAddress, runnable);
}
/**
* @param inetAddress IP address of the player
*/
public void remove(final InetAddress inetAddress) {
queuedPlayers.remove(inetAddress);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,9 @@ package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
@@ -33,12 +35,13 @@ import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
public interface FallbackUser<X, Y> {
public interface FallbackUser<T> {
@NotNull Fallback getFallback();
@NotNull X getPlayer();
@NotNull Y getConnection();
/**
* @return Per-platform connection object used internally
*/
@NotNull T getConnection();
@NotNull Channel getChannel();
@@ -55,6 +58,8 @@ public interface FallbackUser<X, Y> {
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "2.0.17")
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
@@ -72,13 +77,25 @@ public interface FallbackUser<X, Y> {
*
* @param msg Message to send to the player
*/
void write(final @NotNull Object msg);
default void write(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().writeAndFlush(msg, getChannel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
}
/**
* Queues a buffered message that will be
* sent once all messages are flushed.
*/
void delayedWrite(final @NotNull Object msg);
default void delayedWrite(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().write(msg, getChannel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
}
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,24 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
package xyz.jonesdev.sonar.api.statistics;
import java.net.URL;
import java.net.URLClassLoader;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
final class ExternalClassLoader extends URLClassLoader {
import java.util.concurrent.TimeUnit;
/**
* Create a URLClassLoader with the system ClassLoader as parent
*
* @param url URL for of the local file
*/
public ExternalClassLoader(final URL[] url) {
super(url, ClassLoader.getSystemClassLoader().getParent());
}
// Register this URLClassLoader as parallel capable
static {
ClassLoader.registerAsParallelCapable();
}
public interface Counters {
ExpiringCache<Long> LOGINS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
ExpiringCache<Long> CONNECTIONS_PER_SECOND = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.util.Collection;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
@Getter
public final class Notification implements Observable, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
public void sendAttackNotification() {
// No need to do anything if we have no subscribers
if (subscribers.isEmpty()) {
return;
}
// Prepare the title
// TODO: Why do custom title times not work?
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
final Title title = Title.title(
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
// Prepare the chat message
final Component chat = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getNotifications().getNotificationChat()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl()));
// Send the title and chat messages to all online players
for (final String subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
if (audience == null) continue;
audience.showTitle(title);
audience.sendMessage(chat);
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
public interface Observable {
Collection<String> getSubscribers();
/**
* @param name Name of the audience
* @return Whether the audience is subscribed or not
*/
default boolean isSubscribed(final @NotNull String name) {
return getSubscribers().contains(name);
}
/**
* @param name Name of the audience to subscribe
*/
default void subscribe(final String name) {
getSubscribers().add(name);
}
/**
* @param name Name of the audience to unsubscribe
*/
default void unsubscribe(final String name) {
getSubscribers().remove(name);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,36 +23,32 @@ 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.attack.AttackTracker;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Counters;
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 final class Verbose implements JVMProfiler {
public final class Verbose implements Observable, JVMProfiler, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
private int animationIndex;
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
// Run action bar verbose
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
loginsPerSecond.cleanUp(false);
// Clean up all counters
LOGINS_PER_SECOND.cleanUp(false);
CONNECTIONS_PER_SECOND.cleanUp(false);
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) return;
@@ -60,7 +56,7 @@ public final class Verbose implements JVMProfiler {
final Component component = prepareActionBarFormat();
// Send the action bar to all online players
for (final String subscriber : subscribers) {
final Audience audience = audiences.get(subscriber);
final Audience audience = Sonar.AUDIENCES.get(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
}
@@ -78,7 +74,8 @@ public final class Verbose implements JVMProfiler {
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.estimatedSize()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
@@ -100,26 +97,4 @@ public final class Verbose implements JVMProfiler {
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
/**
* @param name Name of the audience
* @return Whether the audience is subscribed or not
*/
public boolean isSubscribed(final @NotNull String name) {
return subscribers.contains(name);
}
/**
* @param name Name of the audience to subscribe
*/
public void subscribe(final String name) {
subscribers.add(name);
}
/**
* @param name Name of the audience to unsubscribe
*/
public void unsubscribe(final String name) {
subscribers.remove(name);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
+5 -1
View File
@@ -13,6 +13,7 @@ bukkit {
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
commands {
register("sonar") {
@@ -31,13 +32,16 @@ dependencies {
compileOnly(project(":common"))
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bukkit:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,11 +17,14 @@
package xyz.jonesdev.sonar.bukkit;
import com.alessiodp.libby.BukkitLibraryManager;
import lombok.Getter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
@@ -34,7 +37,9 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
super(plugin, new BukkitLibraryManager(
plugin, plugin.getDataFolder().getName()),
plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
}
@@ -78,5 +83,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
TrafficCounter::reset, 20L, 20L);
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
getFallback().getQueue().getPollTask(), 10L, 10L);
// Register verbose service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Sonar.get().getVerboseHandler()::update, 4L, 4L);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,11 +32,11 @@ public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
+5 -1
View File
@@ -9,6 +9,7 @@ bungee {
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
}
dependencies {
@@ -19,10 +20,13 @@ dependencies {
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,11 +17,14 @@
package xyz.jonesdev.sonar.bungee;
import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
@@ -30,12 +33,14 @@ import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.concurrent.TimeUnit;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
INSTANCE = this;
}
@@ -85,5 +90,17 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), TrafficCounter::reset,
1L, 1L, TimeUnit.SECONDS);
// Register queue service
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
500L, 500L, TimeUnit.MILLISECONDS);
// Register verbose service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
200L, 200L, TimeUnit.MILLISECONDS);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,11 +32,11 @@ public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,20 +17,86 @@
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import lombok.Getter;
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.ProxyServer;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
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 FallbackHandlerBoss extends HandlerBoss {
private ChannelWrapper channel;
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private static final @NotNull BungeeCord BUNGEE = BungeeCord.getInstance();
private static final Field CHANNEL_WRAPPER_FIELD;
static {
try {
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
CHANNEL_WRAPPER_FIELD.setAccessible(true);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
private ChannelWrapper channelWrapper;
private PacketHandler handler;
private @Nullable FallbackUserWrapper user;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void setHandler(final @NotNull PacketHandler handler) {
@@ -41,36 +107,250 @@ public final class FallbackHandlerBoss extends HandlerBoss {
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel = new ChannelWrapper(ctx);
super.channelActive(ctx);
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel.markClosed();
super.channelInactive(ctx);
}
}
@Override
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
handler.writabilityChanged(channel);
super.channelWritabilityChanged(ctx);
// Use the same channel wrapper as the "real" HandlerBoss
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(this);
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime());
}
}
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
// Handle Handshake packets
if (packetWrapper.packet instanceof Handshake) {
final Handshake handshake = (Handshake) packetWrapper.packet;
// Check if the host is invalid
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Set protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
}
// Handle StatusRequest packets
if (packetWrapper.packet instanceof StatusRequest) {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new CorruptedFrameException("Duplicate status packet");
}
receivedStatusPacket = true;
}
// Handle LoginRequest packets
if (packetWrapper.packet instanceof LoginRequest) {
final LoginRequest loginRequest = (LoginRequest) packetWrapper.packet;
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || user != null) {
throw new CorruptedFrameException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
try {
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Use the ChannelWrapper's remote address to automatically handle proxy protocol
final InetAddress inetAddress = ((InetSocketAddress) channelWrapper.getRemoteAddress()).getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Check if the verification is enabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
super.channelRead(ctx, msg);
return;
}
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
super.channelRead(ctx, msg);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
final UUID 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.channelRead(ctx, msg);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
super.channelRead(ctx, msg);
return;
}
// Create wrapped Fallback user
user = new FallbackUserWrapper(
FALLBACK, this, channel, channel.pipeline(),
inetAddress, protocolVersion);
// Perform default BungeeCord checks
if (BUNGEE.config.isEnforceSecureProfile()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
closeWith(getKickPacket(Component.text(BUNGEE.getTranslation("secure_profile_required"))));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
closeWith(getKickPacket(BUNGEE.getTranslation("secure_profile_expired")));
return;
}
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
closeWith(getKickPacket(BUNGEE.getTranslation("secure_profile_invalid")));
return;
}
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
// Check if the player is already connected to the proxy but still tries to verify
final int limit = BUNGEE.config.getPlayerLimit();
if (limit > 0 && BUNGEE.getOnlineCount() >= limit) {
closeWith(getKickPacket(BUNGEE.getTranslation("proxy_full")));
return;
} else if (!BUNGEE.getConfig().isOnlineMode() && BUNGEE.getPlayer(loginRequest.getData()) != null) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (user.getPipeline().get(PACKET_ENCODER) == null
|| user.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
return;
}
}
super.channelRead(ctx, msg);
}
}
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) {
if (ctx.channel().isActive()) {
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
try {
@@ -80,8 +360,50 @@ public final class FallbackHandlerBoss extends HandlerBoss {
}
}
channel.markClosed();
channelWrapper.markClosed();
ctx.close();
}
}
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
public static @NotNull Kick getKickPacket(final @NotNull String reason) {
final Component component = Component.text(reason);
return getKickPacket(component);
}
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
if (cachedKickPacket == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
}
return cachedKickPacket;
}
public void closeWith(final Object msg) {
closeWith(channelWrapper, protocolVersion, msg);
}
// Mostly taken from Velocity
public static void closeWith(final @NotNull ChannelWrapper channelWrapper,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channelWrapper.getHandle().isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
channelWrapper.getHandle().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
channelWrapper.getHandle().eventLoop().schedule(() -> {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -1,345 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
public final class FallbackInitialHandler extends InitialHandler {
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
super(bungee, listener);
this.bungee = bungee;
}
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
@Getter
private ChannelWrapper channelWrapper;
private final @NotNull BungeeCord bungee;
private @Nullable FallbackUserWrapper user;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
@Override
public void handle(final StatusRequest statusRequest) throws Exception {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new CorruptedFrameException("Duplicate status packet");
}
receivedStatusPacket = true;
// Run the rest of the method asynchronously
CompletableFuture.runAsync(() -> {
// The channel always stays open because the client sends
// a StatusRequest and a Ping packet after one another
if (!isConnected()) {
throw new CorruptedFrameException("Not connected anymore");
}
try {
super.handle(statusRequest);
} catch (Exception exception) {
throw new DecoderException(exception);
}
});
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
// Increase joins per second for the action bar verbose
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || user != null) {
throw new CorruptedFrameException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
final int protocolId = getHandshake().getProtocolVersion();
protocolVersion = ProtocolVersion.fromId(protocolId);
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
try {
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
final InetAddress inetAddress = getAddress().getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Check if the verification is enabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
super.handle(loginRequest);
return;
}
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
super.handle(loginRequest);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
// Check if the player is already verified
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
super.handle(loginRequest);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
super.handle(loginRequest);
return;
}
// Create wrapped Fallback user
user = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress, protocolVersion
);
// Perform default BungeeCord checks
if (bungee.config.isEnforceSecureProfile()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
disconnect(bungee.getTranslation("secure_profile_required"));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
disconnect(bungee.getTranslation("secure_profile_expired"));
return;
}
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
disconnect(bungee.getTranslation("secure_profile_invalid"));
return;
}
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData(), inetAddress));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
// Check if the player is already connected to the proxy but still tries to verify
final int limit = bungee.config.getPlayerLimit();
if (limit > 0 && bungee.getOnlineCount() >= limit) {
disconnect(bungee.getTranslation("proxy_full"));
return;
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (user.getPipeline().get(PACKET_ENCODER) == null
|| user.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
if (cachedKickPacket == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
}
return cachedKickPacket;
}
// Mostly taken from Velocity
public void closeWith(final Object msg) {
if (channelWrapper.getHandle().isActive()) {
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
if (is17) {
channelWrapper.getHandle().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
channelWrapper.getHandle().eventLoop().schedule(() -> {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,23 +19,40 @@ package xyz.jonesdev.sonar.bungee.fallback;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
private static final Field CHANNEL_FIELD;
static {
try {
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
CHANNEL_FIELD.setAccessible(true);
} catch (NoSuchFieldException exception) {
throw new ReflectiveOperationException(exception);
}
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull LoginEvent event) {
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
// Check if the number of online players using the same IP address as
@@ -50,9 +67,10 @@ public final class FallbackListener implements Listener {
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
final PendingConnection connection = event.getConnection();
FallbackHandlerBoss.closeWith((ChannelWrapper) CHANNEL_FIELD.get(connection),
ProtocolVersion.fromId(connection.getVersion()), FallbackHandlerBoss.getKickPacket(component));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,11 +19,9 @@ package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@@ -34,35 +32,22 @@ import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
public final class FallbackUserWrapper implements FallbackUser<FallbackHandlerBoss> {
private final Fallback fallback;
private final ChannelWrapper player;
private final FallbackInitialHandler connection;
private final FallbackHandlerBoss connection;
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
public void write(final @NotNull Object packet) {
if (channel.isActive()) {
channel.writeAndFlush(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
@Override
public void delayedWrite(final @NotNull Object packet) {
if (channel.isActive()) {
channel.write(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.bungee.fallback.injection;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@@ -55,16 +56,18 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
}
}
// Mostly taken from BungeeCord
// Taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
try {
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
} catch (ChannelException ignored) {
// Use TCPNoDelay
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
} catch (ChannelException exception) {
// IP_TOS is not supported (Windows XP / Windows Server 2003)
}
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,13 +25,12 @@ import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.event.ClientConnectEvent;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
import java.lang.reflect.Field;
import java.net.SocketAddress;
@@ -56,7 +55,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
}
}
// Mostly taken from BungeeCord
// Taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
@@ -65,32 +64,27 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
channel.close();
} else {
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
return;
}
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
channel.close();
} else {
try {
BASE.initChannel(channel);
} catch (Exception exception) {
exception.printStackTrace(System.err);
channel.close();
return;
}
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
channel.close();
return;
}
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
}
}
BASE.initChannel(channel);
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
Protocol.HANDSHAKE, true, BUNGEE.getProtocolVersion()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
Protocol.HANDSHAKE, true, BUNGEE.getProtocolVersion()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new InitialHandler(BUNGEE, listener));
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.Library;
import com.alessiodp.libby.LibraryManager;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@UtilityClass
class LibraryLoader {
void loadLibraries(final @NotNull LibraryManager libraryManager) {
libraryManager.addMavenCentral();
libraryManager.addJitPack();
libraryManager.loadLibraries(
// Simple-YAML
// TODO: use a different library for config management?
Library.builder()
.groupId("com{}github{}Carleslc{}Simple-YAML")
.artifactId("Simple-Yaml")
.version("1.8.4")
.relocate("com{}simpleyaml", "xyz{}jonesdev{}sonar{}libs{}yaml")
.build(),
// Gson
Library.builder()
.groupId("com{}google{}code{}gson")
.artifactId("gson")
.version("2.10.1")
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.build(),
// ORMLite
Library.builder()
.groupId("com{}j256{}ormlite")
.artifactId("ormlite-jdbc")
.version("6.1")
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
.build(),
// MySQL
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.build()
);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -31,9 +32,9 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.service.ServiceRepository;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
@@ -47,24 +48,32 @@ public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
@Setter
private Notification notificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final LibraryManager libraryManager;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
private final SystemTimer launchTimer = new SystemTimer();
public SonarBootstrap(final @NotNull T plugin,
final LibraryManager libraryManager,
final File dataDirectory,
final SonarPlatform platform) {
// Set the Sonar API
SonarSupplier.set(this);
// Set the plugin instance before anything else
// Store our library manager for the API
this.libraryManager = libraryManager;
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager);
// Set the plugin instance
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.platform = platform;
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
}
@@ -86,9 +95,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().info("Successfully initialized components in {}s!", launchTimer);
getLogger().info("Enabling all tasks and features...");
// Start all service threads
ServiceRepository.register();
try {
// Run the per-platform initialization method
enable();
@@ -134,8 +140,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
public void shutdown() {
getLogger().info("Starting shutdown process...");
// Shut down service threads
ServiceRepository.shutdown();
// ...
getLogger().info("Successfully shut down. Goodbye!");
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,7 +53,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// General
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser<?, ?> user;
private final @NotNull FallbackUser<?> user;
private final String username;
private final UUID playerUuid;
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
@@ -87,7 +87,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
SUCCESS
}
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
public FallbackVerificationHandler(final @NotNull FallbackUser<?> user,
final @NotNull String username,
final @NotNull UUID playerUuid) {
this.user = user;
@@ -212,14 +212,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?> user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackUser<?, ?> user, final ByteBuf content) {
private static boolean validateClientBrand(final @NotNull FallbackUser<?> user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,11 +31,11 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser<?, ?> user;
private final FallbackUser<?> user;
private final FallbackPacketListener listener;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
public FallbackPacketDecoder(final @NotNull FallbackUser<?> user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.listener = listener;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -222,7 +222,8 @@ public class MapInfoPreparer {
final int index = y * image.getWidth() + x;
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
// Set color of pixel to random color from the palette
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 : RANDOM.nextInt(Byte.MAX_VALUE)
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 :
RANDOM.nextInt(Byte.MAX_VALUE)
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
// Write pixel color to buffer
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,7 +53,7 @@ public final class PreparedMapInfo {
this.modern = new MapData(info);
}
public void write(final @NotNull FallbackUser<?, ?> user) {
public void write(final @NotNull FallbackUser<?> user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapData data : legacy) {
user.delayedWrite(data);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Sonar Contributors
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff. Show more