Compare commits

...
8 Commits
Author SHA1 Message Date
Jones 915e3f593a Release 2.0.20 (#224) 2024-03-01 15:31:01 +01:00
Jones ef525ce718 Only calculate mitigation stats if webhook is enabled (#223) 2024-03-01 15:14:52 +01:00
Jones 0fbb71a09f Also check protocols on BungeeCord servers (#222) 2024-03-01 14:56:36 +01:00
Jones 6ebe44bad3 Drop connections from unknown protocol versions (#221) 2024-03-01 14:51:43 +01:00
Jones 153d56eeb4 Implement experimental support for MariaDB (#220)
* feat: add MariaDB as another db type in the config

* fix: make sure to disable logging for unnecessary ORM stuff

* fix: use pooled connection source to remove unnecessary reflection

* feat: support MariaDB (experimental)
2024-02-29 17:56:58 +01:00
Jones c214d43b3e Use correct multiple-byte unit for formatting (#219) 2024-02-25 12:58:02 +01:00
Jones f3444de4b8 Optimize codecs by removing unused dimensions (#218) 2024-02-24 23:18:34 +01:00
Jones a5bf0947b5 Remove duplicate audience listener (#217) 2024-02-24 14:29:28 +01:00
40 changed files with 201 additions and 248 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.19
version=2.0.20
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
@@ -33,8 +34,7 @@ import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
@@ -43,7 +43,7 @@ public interface Sonar {
* 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<>();
@Nullable Audience audience(final @Nullable UUID uniqueId);
/**
* @return The platform the plugin is being run on
@@ -101,42 +101,44 @@ public final class AttackTracker {
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final 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);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
.ifPresent(webhook -> webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
}));
.ifPresent(webhook -> {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final 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);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
});
});
}
// Reset the attack status
@@ -23,12 +23,13 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final UUID uuid;
private final Audience audience;
private final boolean player;
private final Predicate<String> permissionFunction;
@@ -17,6 +17,9 @@
package xyz.jonesdev.sonar.api.config;
import com.j256.ormlite.db.DatabaseType;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import lombok.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -557,10 +560,15 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null);
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver",
"jdbc:mysql://%s:%d/%s", new MysqlDatabaseType()),
MARIADB("org.mariadb.jdbc.Driver",
"jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseType()),
NONE(null, null, null);
private final String driverClassName;
private final String connectionString;
private final DatabaseType databaseType;
}
private Type type;
@@ -19,7 +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.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -32,9 +32,9 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.sql.*;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -71,8 +71,9 @@ public final class VerifiedPlayerController {
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -80,8 +81,11 @@ public final class VerifiedPlayerController {
return;
}
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
this.connectionSource = connectionSource;
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName()),
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
// Create table
try {
@@ -98,83 +102,38 @@ public final class VerifiedPlayerController {
// 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 {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
LOGGER.error("Could not cache entries: {}", exception);
LOGGER.error("Error initializing database: {}", exception);
}
});
} catch (Throwable throwable) {
LOGGER.error("Error setting up database: {}", throwable);
throwable.printStackTrace(System.err);
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
}
}
/**
* Automatically sets up the database connection using the configured credentials
*/
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Prepare the JDBC connection string
// TODO: automatically build best connection string for database type
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
// Use reflection to uncover the driver class
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
// Make sure we send the username and password
final Properties credentials = new Properties();
credentials.put("user", database.getUsername());
credentials.put("password", database.getPassword());
// Return new JDBC connection instance for our controller to handle
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
}
/**
* Uses reflection to load the database driver from the injected library
*
* @param connectionString URL of the database
* @param driverClass Class of the database driver
* @param credentials Credentials for the database
*/
private static Connection invoke(final @NotNull String connectionString,
final @NotNull Class<?> driverClass,
final @NotNull Properties credentials) throws Throwable {
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, connectionString, credentials);
}
/**
* Clear all old entries using the given timestamp.
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
try {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
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();
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear old entries: {}", exception);
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -26,7 +26,7 @@ import java.lang.management.ManagementFactory;
public class JVMProcessInformation {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
public String formatMemory(final long size) {
if (size < 1024L) {
@@ -35,7 +35,8 @@ public class JVMProcessInformation {
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
return String.format("%.1f %sB", formattedSize, unit);
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
return String.format("%.1f %siB", formattedSize, unit);
}
public int getVirtualCores() {
@@ -27,13 +27,14 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
@Getter
public final class Notification implements Observable {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public void sendAttackNotification() {
// No need to do anything if we have no subscribers
@@ -65,8 +66,8 @@ public final class Notification implements Observable {
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted()));
// 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);
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.showTitle(title);
audience.sendMessage(chat);
@@ -20,29 +20,30 @@ package xyz.jonesdev.sonar.api.verbose;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.UUID;
public interface Observable {
Collection<String> getSubscribers();
Collection<UUID> getSubscribers();
/**
* @param name Name of the audience
* @param uuid UUID of the player
* @return Whether the audience is subscribed or not
*/
default boolean isSubscribed(final @NotNull String name) {
return getSubscribers().contains(name);
default boolean isSubscribed(final @NotNull UUID uuid) {
return getSubscribers().contains(uuid);
}
/**
* @param name Name of the audience to subscribe
* @param uuid UUID of the player
*/
default void subscribe(final String name) {
getSubscribers().add(name);
default void subscribe(final UUID uuid) {
getSubscribers().add(uuid);
}
/**
* @param name Name of the audience to unsubscribe
* @param uuid UUID of the player
*/
default void unsubscribe(final String name) {
getSubscribers().remove(name);
default void unsubscribe(final UUID uuid) {
getSubscribers().remove(uuid);
}
}
@@ -29,6 +29,7 @@ import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
@@ -36,7 +37,7 @@ import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
public final class Verbose implements Observable {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
private int animationIndex;
// Run action bar verbose
@@ -48,8 +49,8 @@ public final class Verbose implements Observable {
// Prepare the action bar format component
final Component component = prepareActionBarFormat();
// Send the action bar to all online players
for (final String subscriber : subscribers) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
for (final UUID subscriber : subscribers) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
}
@@ -19,18 +19,20 @@ package xyz.jonesdev.sonar.bukkit;
import com.alessiodp.libby.BukkitLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.util.Objects;
import java.util.UUID;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@@ -48,6 +50,14 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
@Override
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return bukkitAudiences.console();
}
return bukkitAudiences.player(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -81,9 +91,6 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
CachedBandwidthStatistics::reset, 20L, 20L);
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.audience;
import net.kyori.adventure.audience.Audience;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
@@ -19,19 +19,21 @@ package xyz.jonesdev.sonar.bungee;
import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Getter
@@ -48,6 +50,14 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
@Override
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return bungeeAudiences.console();
}
return bungeeAudiences.player(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -84,9 +94,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
1L, 1L, TimeUnit.SECONDS);
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.audience;
import net.kyori.adventure.audience.Audience;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
super(sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender instanceof ProxiedPlayer,
sender::hasPermission);
@@ -96,6 +96,13 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Hook the traffic listener
@@ -61,6 +61,12 @@ class LibraryLoader {
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.3.3")
.build()
);
}
@@ -19,6 +19,8 @@ package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.j256.ormlite.logger.Level;
import com.j256.ormlite.logger.Logger;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -77,6 +79,8 @@ public abstract class SonarBootstrap<T> implements Sonar {
this.notificationHandler = new Notification();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
// Hide unnecessary debug information
Logger.setGlobalLogLevel(Level.WARNING);
}
public final void initialize() {
@@ -122,8 +126,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (verifiedPlayerController != null
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Prepare cached packets
@@ -83,16 +83,11 @@ public class FallbackPreparer {
public double maxFallDistance;
public void prepare() {
joinGame = new JoinGamePacket(0,
joinGame = new JoinGamePacket(1,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0,
false,
0,
true,
true,
false,
new String[]{"minecraft:overworld"},
"minecraft:overworld");
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld");
maxFallDistance = 0;
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -33,6 +34,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class LoginSuccessPacket implements FallbackPacket {
@@ -57,6 +57,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
@@ -66,6 +67,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
@@ -75,6 +77,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
@@ -84,6 +87,7 @@ public final class JoinGamePacket implements FallbackPacket {
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
@@ -95,6 +99,7 @@ public final class JoinGamePacket implements FallbackPacket {
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
@@ -111,6 +116,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
@@ -128,6 +134,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
@@ -151,6 +158,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
@@ -178,6 +186,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
return;
}
if (protocolVersion.equals(MINECRAFT_1_20)) {
@@ -198,6 +207,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
return;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
@@ -31,13 +31,13 @@ public final class NotifyCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getName());
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsUnsubscribed());
return;
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsSubscribed());
SONAR.getNotificationHandler().subscribe(invocation.getSender().getName());
SONAR.getNotificationHandler().subscribe(invocation.getSender().getUuid());
}
}
@@ -32,8 +32,8 @@ public final class VerboseCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
// Reset ActionBar component when unsubscribing
invocation.getSender().getAudience().sendActionBar(Component.empty());
@@ -41,6 +41,6 @@ public final class VerboseCommand extends Subcommand {
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getUuid());
}
}
@@ -40,7 +40,7 @@ attack-tracker:
# Nicméně, IP adresy nemůžou být vystopovány zpátky k hráči, protože Sonar používá UUID na ukládní místo jmeno hráčů
database:
# Typy datábázi se kterýma Sonar bude pracovat
# Typy: NONE, MYSQL (Žádný, MYSQL)
# Typy: NONE, MYSQL, MARIADB
type: NONE
# Host adresa pro verifikování hráčů s SQL databází
host: localhost
@@ -37,7 +37,7 @@ attack-tracker:
# Beachte: IP adressen werden im Klartext gespeichert. Du bist für die Sicherheit der Datenbank verantwortlich
database:
# Typ von Datenbank die Sonar verwenden soll um verifizierte Spielerdaten zu speichern
# Mögliche Typen: NONE, MYSQL
# Mögliche Typen: NONE, MYSQL, MARIADB
type: NONE
# HOST für die Authentifizierung mit der SQL Datenbank
host: localhost
@@ -39,7 +39,7 @@ attack-tracker:
# Note: IP addresses are saved in plain text. You are responsible for keeping your database safe!
database:
# Type of database Sonar uses to store verified players
# Possible types: NONE, MYSQL
# Possible types: NONE, MYSQL, MARIADB
type: NONE
# Host address for authenticating with the SQL database
host: localhost
@@ -39,7 +39,7 @@ attack-tracker:
# შენიშვნა: IP მისამართები ინახება უბრალო ტექსტში. თქვენ ხართ პასუხისმგებელი თქვენი მონაცემთა ბაზის უსაფრთხოებაზე!
database:
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს ვერიფირებული მოთამაშეების შესანახად
# შესაძლო ტიპები: NONE, MYSQL
# შესაძლო ტიპები: NONE, MYSQL, MARIADB
type: NONE
# ჰოსტის მისამართი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
host: localhost
@@ -39,7 +39,7 @@ attack-tracker:
# Примечание: IP-адреса сохраняются в виде обычного текста. Вы сами отвечаете за сохранность своей базы данных!
database:
# Тип базы данных, которую Sonar использует для хранения проверенных игроков.
# Возможные типы: NONE, MYSQL
# Возможные типы: NONE, MYSQL, MARIADB
type: NONE
# Адрес хоста для аутентификации в базе данных SQL
host: localhost
@@ -19,7 +19,9 @@ package xyz.jonesdev.sonar.velocity;
import com.alessiodp.libby.VelocityLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
@@ -30,6 +32,9 @@ import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Getter
@@ -43,6 +48,19 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
INSTANCE = this;
}
/**
* Custom wrapper for Velocity audiences
*/
public static final Map<UUID, Audience> AUDIENCES = new ConcurrentHashMap<>();
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return getPlugin().getServer().getConsoleCommandSource();
}
return AUDIENCES.get(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -79,6 +97,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
// TODO: find a proper way of handling Audiences on Velocity
// Register audience register listener
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
@@ -22,17 +22,17 @@ import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
public final class AudienceListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PostLoginEvent event) {
Sonar.AUDIENCES.put(event.getPlayer().getUsername(), event.getPlayer());
SonarVelocity.AUDIENCES.put(event.getPlayer().getUniqueId(), event.getPlayer());
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull DisconnectEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getUsername());
SonarVelocity.AUDIENCES.remove(event.getPlayer().getUniqueId());
}
}
@@ -24,7 +24,7 @@ import xyz.jonesdev.sonar.api.command.InvocationSource;
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
sender,
sender instanceof Player,
sender::hasPermission);
@@ -77,6 +77,13 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion().getProtocol());
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol");
}
// Store the handshake packet
handshakePacket = handshake;
// Hook the traffic listener