From be901a31d87a6dca00e380e56d694fea99e367f4 Mon Sep 17 00:00:00 2001 From: itqop Date: Sun, 9 Nov 2025 15:30:56 +0300 Subject: [PATCH] refactor: refactor code --- src/main/java/org/itqop/whitelist/Config.java | 4 +- .../itqop/whitelist/WhitelistApiClient.java | 63 ++++++++++-- .../whitelist/WhitelistEventHandler.java | 97 ++++++++++++++----- 3 files changed, 129 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/itqop/whitelist/Config.java b/src/main/java/org/itqop/whitelist/Config.java index 269576e..d7d9ee5 100644 --- a/src/main/java/org/itqop/whitelist/Config.java +++ b/src/main/java/org/itqop/whitelist/Config.java @@ -15,8 +15,8 @@ public final class Config { .define("apiKey", "your-secret-api-key"); private static final ModConfigSpec.IntValue REQUEST_TIMEOUT = BUILDER - .comment("Request timeout in seconds") - .defineInRange("requestTimeout", 30, 5, 300); + .comment("Request timeout in seconds (recommended: 3-5 to prevent server lag)") + .defineInRange("requestTimeout", 5, 1, 60); private static final ModConfigSpec.BooleanValue ENABLE_LOGGING = BUILDER .comment("Enable detailed API logging") diff --git a/src/main/java/org/itqop/whitelist/WhitelistApiClient.java b/src/main/java/org/itqop/whitelist/WhitelistApiClient.java index 30645c8..e7846da 100644 --- a/src/main/java/org/itqop/whitelist/WhitelistApiClient.java +++ b/src/main/java/org/itqop/whitelist/WhitelistApiClient.java @@ -22,6 +22,12 @@ public class WhitelistApiClient { private static final Logger LOGGER = LogUtils.getLogger(); private static final Gson GSON = new GsonBuilder().create(); + // Circuit breaker to prevent log spam + private volatile long lastErrorLogTime = 0; + private volatile int consecutiveErrors = 0; + private static final long ERROR_LOG_COOLDOWN_MS = 60_000; // 1 minute + private static final int ERROR_THRESHOLD = 3; + private volatile HttpClient http; private volatile String baseUrl; @@ -66,7 +72,10 @@ public class WhitelistApiClient { .thenApply(resp -> { if (resp == null) return false; int code = resp.statusCode(); - if (code == 201) return true; + if (code == 201) { + resetErrorCounter(); + return true; + } logHttpError("POST /add", code, resp.body()); return false; }) @@ -99,6 +108,7 @@ public class WhitelistApiClient { return null; } try { + resetErrorCounter(); JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject(); return WhitelistEntry.fromJson(json); } catch (Exception e) { @@ -120,7 +130,10 @@ public class WhitelistApiClient { .thenApply(resp -> { if (resp == null) return false; int code = resp.statusCode(); - if (code == 204) return true; + if (code == 204) { + resetErrorCounter(); + return true; + } logHttpError("POST /remove", code, resp.body()); return false; }) @@ -143,16 +156,17 @@ public class WhitelistApiClient { logHttpError("POST /check", code, response.body()); return new CheckResponse(false, false); } + resetErrorCounter(); JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); boolean isWhitelisted = json.has("is_whitelisted") && json.get("is_whitelisted").getAsBoolean(); return new CheckResponse(true, isWhitelisted); } catch (Exception e) { - LOGGER.error("Failed to parse checkPlayer response for '{}': {}", playerName, e.getMessage(), e); + LOGGER.warn("Failed to parse checkPlayer response for '{}': {}", playerName, e.getMessage()); return new CheckResponse(false, false); } }) .exceptionally(ex -> { - LOGGER.error("Failed to check player '{}': {}", playerName, ex.getMessage(), ex); + logApiError("Failed to check player '" + playerName + "'", ex); return new CheckResponse(false, false); }); } @@ -219,10 +233,43 @@ public class WhitelistApiClient { } private void logHttpError(String endpoint, int statusCode, String responseBody) { - if (Config.enableLogging) { - LOGGER.error("API request failed: {} returned {} - Response: {}", endpoint, statusCode, responseBody); - } else { - LOGGER.error("API request failed: {} returned {}", endpoint, statusCode); + consecutiveErrors++; + + // Log only first error and then once per minute to prevent spam + long now = System.currentTimeMillis(); + if (consecutiveErrors == 1 || (now - lastErrorLogTime) > ERROR_LOG_COOLDOWN_MS) { + if (Config.enableLogging) { + LOGGER.warn("API request failed: {} returned {} - Response: {}", endpoint, statusCode, responseBody); + } else { + LOGGER.warn("API request failed: {} returned {}", endpoint, statusCode); + } + lastErrorLogTime = now; + + if (consecutiveErrors > ERROR_THRESHOLD) { + LOGGER.warn("API has failed {} times consecutively. Further errors will be throttled.", consecutiveErrors); + } + } + } + + private void logApiError(String message, Throwable ex) { + consecutiveErrors++; + + // Log only first error and then once per minute to prevent spam + long now = System.currentTimeMillis(); + if (consecutiveErrors == 1 || (now - lastErrorLogTime) > ERROR_LOG_COOLDOWN_MS) { + LOGGER.warn("{}: {} (errors: {})", message, ex.getMessage(), consecutiveErrors); + lastErrorLogTime = now; + + if (consecutiveErrors > ERROR_THRESHOLD) { + LOGGER.warn("API has failed {} times consecutively. Further errors will be throttled.", consecutiveErrors); + } + } + } + + private void resetErrorCounter() { + if (consecutiveErrors > 0) { + LOGGER.info("API connection restored after {} consecutive errors", consecutiveErrors); + consecutiveErrors = 0; } } diff --git a/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java b/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java index 71d0a54..0db477c 100644 --- a/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java +++ b/src/main/java/org/itqop/whitelist/WhitelistEventHandler.java @@ -1,51 +1,98 @@ package org.itqop.whitelist; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.neoforge.event.RegisterCommandsEvent; -import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import com.mojang.authlib.GameProfile; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.slf4j.Logger; +import com.mojang.logging.LogUtils; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @EventBusSubscriber(modid = Whitelist.MODID) public class WhitelistEventHandler { + private static final Logger LOGGER = LogUtils.getLogger(); @SubscribeEvent public static void registerCommands(RegisterCommandsEvent event) { WhitelistCommands.register(event.getDispatcher()); } - @SubscribeEvent + @SubscribeEvent(priority = EventPriority.HIGHEST) public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { if (!Config.enableWhitelist) return; - if (!(event.getEntity() instanceof ServerPlayer sp)) return; + if (!(event.getEntity() instanceof ServerPlayer player)) return; - String name = sp.getGameProfile().getName(); - WhitelistApiClient.get().checkPlayer(name).thenAccept(resp -> { - if (resp != null && resp.isSuccess() && !resp.isWhitelisted()) { - // Check if server is still available (player might have disconnected) - if (sp.server == null) return; + GameProfile profile = player.getGameProfile(); + String playerName = profile.getName(); - sp.server.execute(() -> { - // Double-check connection is still valid - if (sp.connection == null) return; + // Check whitelist synchronously with timeout protection + CompletableFuture checkFuture = + WhitelistApiClient.get().checkPlayer(playerName); - Component link = Component.literal("https://hubmc.org/shop") - .withStyle(style -> style - .withUnderlined(true) - .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://hubmc.org/shop")) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("Открыть в браузере"))) - ); + try { + // Block login with timeout + WhitelistApiClient.CheckResponse response = checkFuture + .completeOnTimeout( + new WhitelistApiClient.CheckResponse(false, false), + Config.requestTimeout, + TimeUnit.SECONDS + ) + .join(); - Component msg = Component.literal("Вас нету в белом списке сервера.\n") - .append(Component.literal("Для входа необходимо приобрести проходку на сайте ")) - .append(link); + // If check failed or player not whitelisted, kick immediately + if (!response.isSuccess() || !response.isWhitelisted()) { + kickPlayer(player, createKickMessage(response.isSuccess())); + } - sp.connection.disconnect(msg); - }); + } catch (Exception e) { + // On any error, deny access + LOGGER.error("Whitelist check error for '{}': {}", playerName, e.getMessage()); + kickPlayer(player, Component.literal("Whitelist service error. Please contact administrator.")); + } + } + + private static void kickPlayer(ServerPlayer player, Component message) { + if (player.server == null || player.connection == null) return; + + // Send disconnect message with delay to ensure client receives it + // Without delay, rapid reconnects may fail to show the message + player.server.execute(() -> { + if (player.connection != null) { + try { + // Small delay to ensure message packet is sent before connection close + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + player.connection.disconnect(message); + LOGGER.info("Player '{}' kicked from whitelist", player.getGameProfile().getName()); } }); } + + private static Component createKickMessage(boolean apiAvailable) { + if (!apiAvailable) { + return Component.literal("Whitelist service unavailable. Please try again later."); + } + + Component link = Component.literal("https://hubmc.org/shop") + .withStyle(style -> style + .withUnderlined(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://hubmc.org/shop")) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.literal("Открыть в браузере"))) + ); + + return Component.literal("Вас нету в белом списке сервера.\n") + .append(Component.literal("Для входа необходимо приобрести проходку на сайте ")) + .append(link); + } }