Compare commits

...
9 Commits
Author SHA1 Message Date
Jones 7bb231ae0d Release 2.0.22 (#234) 2024-03-05 18:07:55 +01:00
Jones a57dfc4e98 Correctly close database connection (#233) 2024-03-04 17:29:39 +01:00
Jones 618db4804b Recode bStats metrics implementation (#232)
* feat: add verification timing and map captcha timing to bstats charts

* fix: use correct char ids for bStats

* fix: implement shutdown method to correctly stop certain services

* style: small codestyle improvements
2024-03-03 18:31:27 +01:00
Jones 613f793dbf Relocate database drivers to avoid compatibility issues (#231) 2024-03-03 10:03:48 +01:00
Jones 40964d431c Release 2.0.21 (#230) 2024-03-01 22:58:39 +01:00
Jones b9deff9c0b Remove unused protocol ids to avoid exploits (#229) 2024-03-01 22:55:33 +01:00
Jones e3f27171cf Fix NullPointerException (#227) 2024-03-01 22:50:46 +01:00
Jones d3601e10fe Remove pipelines if they are not needed anymore (#226)
* fix: optimize counting of connections and logins

* fix: remove Sonar's pipelines if they aren't needed anymore
2024-03-01 19:25:13 +01:00
Jones 258e425ba1 Use correct packet limits for PluginMessagePacket (#225) 2024-03-01 17:04:24 +01:00
18 changed files with 197 additions and 44 deletions

No files matched your search

+2
View File
@@ -80,6 +80,8 @@ tasks {
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
// TODO: actually fix this :/
// We have to be careful here, so we don't accidentally break adventure
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.20
version=2.0.22
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
@@ -18,8 +18,6 @@
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;
@@ -28,6 +26,8 @@ import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -420,8 +420,13 @@ public final class SonarConfiguration {
private Timing timing;
@Getter
@RequiredArgsConstructor
public enum Timing {
ALWAYS, DURING_ATTACK, NEVER
ALWAYS("Always"),
DURING_ATTACK("During Attack"),
NEVER("Never");
private final String displayName;
}
private final Map map = new Map();
@@ -560,13 +565,10 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
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);
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
NONE(null, null);
private final String driverClassName;
private final String connectionString;
private final DatabaseType databaseType;
}
@@ -81,6 +81,9 @@ public final class VerifiedPlayerController {
return;
}
// Initialize MySQL/MariaDB driver
database.getType().getDatabaseType().loadDriver();
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
@@ -116,6 +119,21 @@ public final class VerifiedPlayerController {
}
}
/**
* Closes the connection if instantiated
*/
public void close() {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
try {
// Properly close the connection
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
}
}
}
/**
* Clear all old entries using the given timestamp.
*/
@@ -20,13 +20,15 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Getter
@RequiredArgsConstructor
public enum ProtocolVersion {
UNKNOWN(-1),
LEGACY(-2),
MINECRAFT_1_7_2(4),
MINECRAFT_1_7_6(5),
MINECRAFT_1_8(47),
@@ -70,10 +72,8 @@ public enum ProtocolVersion {
private final int protocol;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
static {
{
@@ -90,12 +90,11 @@ public enum ProtocolVersion {
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
for (final ProtocolVersion value : values()) {
if (!value.isUnknown() && !value.isLegacy()) {
if (!value.isUnknown()) {
versions.add(value);
}
}
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
}
}
@@ -111,9 +110,4 @@ public enum ProtocolVersion {
public boolean isUnknown() {
return this == UNKNOWN;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLegacy() {
return this == LEGACY;
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
}
}
@@ -22,6 +22,7 @@ import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
@@ -82,11 +83,18 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
@@ -103,4 +111,10 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Sonar.get().getVerboseHandler()::update, 4L, 4L);
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -22,6 +22,7 @@ import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
@@ -82,11 +83,18 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
@@ -109,4 +117,10 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -45,6 +45,7 @@ import java.util.UUID;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
@@ -96,6 +97,8 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
@@ -103,8 +106,6 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// 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
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
@@ -136,12 +137,14 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
@@ -171,6 +174,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
@@ -184,6 +188,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
@@ -61,12 +61,14 @@ class LibraryLoader {
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.3.3")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()
);
}
@@ -156,14 +156,23 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
// Reinitialize database controller
if (verifiedPlayerController != null) {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
}
public void shutdown() {
public final void shutdown() {
// Initialize shutdown process
getLogger().info("Starting shutdown process...");
// ...
// Close database connection
verifiedPlayerController.close();
// Run the per-platform disable method
disable();
// Thank the user for using Sonar
getLogger().info("Successfully shut down. Goodbye!");
}
public abstract void disable();
}
@@ -44,8 +44,7 @@ import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
@Getter
@ToString(of = {"protocolVersion", "inetAddress"})
@@ -128,7 +127,7 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@Override
public void fail(@Nullable String reason) {
public void fail(final @Nullable String reason) {
if (channel.isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
@@ -238,4 +237,17 @@ public final class FallbackUserWrapper implements FallbackUser {
channel.close();
}
}
/**
* Removes Sonar's pipelines from a channel to save
* unnecessary RAM and CPU usage
*
* @param pipeline Pipeline object of the channel
*/
public static void deject(final @NotNull ChannelPipeline pipeline) {
// Remove Sonar's pipelines if they exist
if (pipeline.get(FALLBACK_HANDLER) != null) {
pipeline.remove(FALLBACK_HANDLER);
}
}
}
@@ -373,7 +373,7 @@ public enum FallbackPacketRegistry {
PacketRegistry() {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isLegacy() && !version.isUnknown()) {
if (!version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
}
}
@@ -42,17 +42,17 @@ public final class PluginMessagePacket extends DeferredByteBufHolder implements
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 8;
return 4;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
return 0xFFF; // strict size limit
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
channel = readString(byteBuf, 512);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
channel = transformLegacyToModernChannel(channel);
@@ -24,28 +24,31 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
public final class GlobalSonarStatistics implements SonarStatistics {
private static final Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
private static final Cache<Integer, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
private static final Cache<Integer, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final AtomicInteger ACTION_COUNTER = new AtomicInteger(Integer.MIN_VALUE);
/**
* Helper methods that make it easier to count new statistics
*/
@ApiStatus.Internal
public static void countConnection() {
CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
CONNECTIONS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
}
@ApiStatus.Internal
public static void countLogin() {
LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
LOGINS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
totalJoinedPlayers++;
}
@@ -20,6 +20,8 @@ package xyz.jonesdev.sonar.velocity;
import com.alessiodp.libby.VelocityLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
@@ -85,11 +87,18 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
metrics = getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
@@ -119,4 +128,10 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject(getPlugin().getServer());
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -41,6 +41,7 @@ import java.util.UUID;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
@@ -115,12 +116,14 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
@@ -150,6 +153,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
@@ -163,6 +167,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}