From 46d597d6ff6931b4e35336afb54fe0894a7bb8ab Mon Sep 17 00:00:00 2001 From: Peijun Zhao Date: Sun, 10 May 2026 15:54:48 +0800 Subject: [PATCH] feat: critical fixes + health API + webhook + attack reports - Fix busy-wait loop in ServerGuardService (removed while(true)) - Fix sync blocking on async Future (use thenAccept instead of .get()) - Add startup banner with system info and config validation - Add HealthService: /api/health, /api/health/live, /api/health/ready - Add RequestLoggingFilter for HTTP request monitoring - Add CorsConfig for cross-origin support - Add WebhookService (Discord/Slack notifications) - Add AttackReportService with HTML report generation - New endpoints: /api/attacks/recent, /api/attacks/report, /api/webhook/test - Upgrade Lombok 1.18.30 to 1.18.38 for Java 25+ compatibility --- .../java/com/aluer/config/CorsConfig.java | 27 ++ .../notification/AttackReportService.java | 121 +++++++++ .../aluer/notification/WebhookService.java | 122 +++++++++ .../com/aluer/service/ServerGuardService.java | 243 +++++++++++------- .../com/aluer/web/DashboardController.java | 58 ++++- .../java/com/aluer/web/HealthService.java | 129 ++++++++++ .../com/aluer/web/RequestLoggingFilter.java | 43 ++++ src/main/resources/application.yml | 8 + 8 files changed, 661 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/aluer/config/CorsConfig.java create mode 100644 src/main/java/com/aluer/notification/AttackReportService.java create mode 100644 src/main/java/com/aluer/notification/WebhookService.java create mode 100644 src/main/java/com/aluer/web/HealthService.java create mode 100644 src/main/java/com/aluer/web/RequestLoggingFilter.java diff --git a/src/main/java/com/aluer/config/CorsConfig.java b/src/main/java/com/aluer/config/CorsConfig.java new file mode 100644 index 0000000..7dc6fcc --- /dev/null +++ b/src/main/java/com/aluer/config/CorsConfig.java @@ -0,0 +1,27 @@ +package com.aluer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration corsConfig = new CorsConfiguration(); + corsConfig.setAllowedOriginPatterns(List.of("*")); + corsConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + corsConfig.setAllowedHeaders(List.of("*")); + corsConfig.setAllowCredentials(true); + corsConfig.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", corsConfig); + return new CorsFilter(source); + } +} diff --git a/src/main/java/com/aluer/notification/AttackReportService.java b/src/main/java/com/aluer/notification/AttackReportService.java new file mode 100644 index 0000000..08deaf5 --- /dev/null +++ b/src/main/java/com/aluer/notification/AttackReportService.java @@ -0,0 +1,121 @@ +package com.aluer.notification; + +import com.aluer.model.AlertEvent; +import com.aluer.model.MetricsData; +import com.google.gson.Gson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedDeque; + +@Service +public class AttackReportService { + private static final Logger logger = LoggerFactory.getLogger(AttackReportService.class); + private final Gson gson = new Gson(); + private final ConcurrentLinkedDeque> recentAttacks = new ConcurrentLinkedDeque<>(); + + @Value("${serverguard.report.dir:./reports}") + private String reportDir; + + public void recordAttack(AlertEvent alert, String sourceIp, String details) { + Map record = new java.util.LinkedHashMap<>(); + record.put("timestamp", System.currentTimeMillis()); + record.put("type", alert.getType().name()); + record.put("sourceIp", sourceIp); + record.put("confidence", alert.getConfidence()); + record.put("message", alert.getMessage()); + record.put("details", details); + recentAttacks.addFirst(record); + while (recentAttacks.size() > 500) { + recentAttacks.pollLast(); + } + } + + public List> getRecentAttacks(int limit) { + List> result = new ArrayList<>(); + int count = 0; + for (Map attack : recentAttacks) { + if (count++ >= limit) break; + result.add(attack); + } + return result; + } + + public String generateHtmlReport() { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("Aluer ServerGuard Attack Report"); + html.append(""); + html.append("

Aluer ServerGuard — Attack Report

"); + html.append("

Generated: ").append(formatTimestamp(System.currentTimeMillis())).append("

"); + + html.append(""); + html.append(""); + html.append(""); + + for (Map attack : recentAttacks) { + double confidence = toDouble(attack.get("confidence")); + String cssClass = confidence > 0.8 ? "critical" : confidence > 0.5 ? "high" : "medium"; + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + } + html.append("
TimeTypeSource IPConfidenceMessage
").append(formatTimestamp(toLong(attack.get("timestamp")))).append("").append(attack.get("type")).append("").append(attack.get("sourceIp")).append("").append(String.format("%.0f%%", confidence * 100)).append("").append(attack.get("message")).append("
"); + return html.toString(); + } + + public File saveHtmlReport() throws IOException { + Files.createDirectories(Path.of(reportDir)); + String filename = "attack-report-" + DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") + .withZone(ZoneId.systemDefault()).format(Instant.now()) + ".html"; + File file = new File(reportDir, filename); + try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { + writer.write(generateHtmlReport()); + } + logger.info("Attack report saved: {}", file.getAbsolutePath()); + return file; + } + + private String formatTimestamp(long millis) { + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(millis)); + } + + private double toDouble(Object value) { + if (value instanceof Number n) return n.doubleValue(); + return 0.0; + } + + private long toLong(Object value) { + if (value instanceof Number n) return n.longValue(); + return 0L; + } +} diff --git a/src/main/java/com/aluer/notification/WebhookService.java b/src/main/java/com/aluer/notification/WebhookService.java new file mode 100644 index 0000000..0887b9e --- /dev/null +++ b/src/main/java/com/aluer/notification/WebhookService.java @@ -0,0 +1,122 @@ +package com.aluer.notification; + +import com.aluer.model.AlertEvent; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +@Service +public class WebhookService { + private static final Logger logger = LoggerFactory.getLogger(WebhookService.class); + private final Gson gson = new Gson(); + + @Value("${serverguard.webhook.discord-url:}") + private String discordWebhookUrl; + + @Value("${serverguard.webhook.slack-url:}") + private String slackWebhookUrl; + + @Value("${serverguard.webhook.enabled:false}") + private boolean enabled; + + public CompletableFuture sendDiscordAlert(AlertEvent alert) { + if (!enabled || discordWebhookUrl.isEmpty()) { + return CompletableFuture.completedFuture(false); + } + + return CompletableFuture.supplyAsync(() -> { + try { + JsonObject embed = new JsonObject(); + embed.addProperty("title", alert.getType().getTitle()); + embed.addProperty("description", alert.getMessage()); + embed.addProperty("color", alert.getConfidence() > 0.8 ? 0xFF0000 : + alert.getConfidence() > 0.5 ? 0xFFA500 : 0xFFFF00); + + JsonObject footer = new JsonObject(); + footer.addProperty("text", "Aluer ServerGuard | " + alert.getType().name()); + embed.add("footer", footer); + + JsonObject payload = new JsonObject(); + com.google.gson.JsonArray embeds = new com.google.gson.JsonArray(); + embeds.add(embed); + payload.add("embeds", embeds); + + sendWebhook(discordWebhookUrl, payload); + logger.debug("Discord webhook sent for alert: {}", alert.getType()); + return true; + } catch (Exception e) { + logger.error("Discord webhook failed: {}", e.getMessage()); + return false; + } + }); + } + + public CompletableFuture sendCustomMessage(String title, String message, String level) { + if (!enabled) { + return CompletableFuture.completedFuture(false); + } + + return CompletableFuture.supplyAsync(() -> { + int color = "critical".equals(level) ? 0xFF0000 : + "warning".equals(level) ? 0xFFA500 : 0x00FF00; + + JsonObject embed = new JsonObject(); + embed.addProperty("title", title); + embed.addProperty("description", message); + embed.addProperty("color", color); + + JsonObject payload = new JsonObject(); + com.google.gson.JsonArray embeds = new com.google.gson.JsonArray(); + embeds.add(embed); + payload.add("embeds", embeds); + + boolean sent = false; + if (!discordWebhookUrl.isEmpty()) { + sent = sendWebhook(discordWebhookUrl, payload); + } + if (!slackWebhookUrl.isEmpty()) { + JsonObject slackPayload = new JsonObject(); + slackPayload.addProperty("text", "*" + title + "*\n" + message); + sent |= sendWebhook(slackWebhookUrl, slackPayload); + } + return sent; + }); + } + + private boolean sendWebhook(String webhookUrl, JsonObject payload) { + try { + URL url = URI.create(webhookUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setDoOutput(true); + + byte[] body = gson.toJson(payload).getBytes(StandardCharsets.UTF_8); + try (OutputStream os = conn.getOutputStream()) { + os.write(body); + } + + int status = conn.getResponseCode(); + return status >= 200 && status < 300; + } catch (Exception e) { + logger.debug("Webhook send failed: {}", e.getMessage()); + return false; + } + } + + public boolean isEnabled() { + return enabled; + } +} diff --git a/src/main/java/com/aluer/service/ServerGuardService.java b/src/main/java/com/aluer/service/ServerGuardService.java index ccb4d09..c404e3a 100644 --- a/src/main/java/com/aluer/service/ServerGuardService.java +++ b/src/main/java/com/aluer/service/ServerGuardService.java @@ -8,6 +8,7 @@ import com.aluer.model.AlertEvent; import com.aluer.model.MetricsData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Service; @@ -15,11 +16,12 @@ import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; @Service public class ServerGuardService implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(ServerGuardService.class); - + private final ServerGuardConfig config; private final ProcessMonitor processMonitor; private final ResourceMonitor resourceMonitor; @@ -33,10 +35,12 @@ public class ServerGuardService implements CommandLineRunner { private final DeepSeekClient deepSeekClient; private final AutoExecutor autoExecutor; private final RconClient rconClient; - + + @Value("${server.port:8080}") + private int serverPort; + private final ScheduledExecutorService scheduler; - private volatile boolean running = true; - private int alertCount = 0; + private final AtomicBoolean running = new AtomicBoolean(true); public ServerGuardService( ServerGuardConfig config, @@ -65,119 +69,193 @@ public class ServerGuardService implements CommandLineRunner { this.deepSeekClient = deepSeekClient; this.autoExecutor = autoExecutor; this.rconClient = rconClient; - - this.scheduler = Executors.newScheduledThreadPool(4); + + this.scheduler = Executors.newScheduledThreadPool(4, r -> { + Thread t = new Thread(r, "serverguard-scheduler"); + t.setDaemon(false); + return t; + }); } @Override - public void run(String... args) throws Exception { - logger.info("========================================"); - logger.info(" Aluer 服务器防护系统 启动中..."); - logger.info("========================================"); - - if (deepSeekClient.isEnabled()) { - logger.info(" DeepSeek AI: 已启用"); - } else { - logger.info(" DeepSeek AI: 未启用 (请配置apiKey以启用)"); - } - + public void run(String... args) { + printBanner(); + validateConfiguration(); + startMonitoring(); - + Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("正在关闭服务器防护系统..."); - running = false; + logger.info("Shutdown signal received, gracefully stopping..."); + running.set(false); scheduler.shutdown(); try { - if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); + Thread.currentThread().interrupt(); } emailAlertService.shutdown(); - logger.info("服务器防护系统已停止"); - })); - - while (running) { - Thread.sleep(1000); + if (rconClient.isConnected()) { + rconClient.close(); + } + logger.info("Aluer ServerGuard stopped"); + }, "serverguard-shutdown")); + } + + private void printBanner() { + logger.info(""); + logger.info("╔══════════════════════════════════════════════════════════════╗"); + logger.info("║ Aluer ServerGuard v1.0.0 ║"); + logger.info("║ AI-Powered Minecraft Server Protection ║"); + logger.info("╠══════════════════════════════════════════════════════════════╣"); + logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version"))); + logger.info("║ Available CPUs: {}{} ║", + String.format("%-32s", Runtime.getRuntime().availableProcessors()), + ""); + logger.info("║ Max Memory: {}{} ║", + String.format("%-32s", (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " MB"), + ""); + logger.info("╠══════════════════════════════════════════════════════════════╣"); + logger.info("║ RCON: {}:{} {}{} ║", + config.getMinecraft().getRcon().getHost(), + config.getMinecraft().getRcon().getPort(), + config.getMinecraft().getRcon().isEnabled() ? "enabled" : "disabled", + " "); + logger.info("║ DeepSeek AI: {}{} ║", + deepSeekClient.isEnabled() ? "enabled (" + config.getAi().getDeepseek().getModel() + ")" : "disabled", + deepSeekClient.isEnabled() ? "" : " "); + logger.info("║ Web Console: http://0.0.0.0:{}{} ║", + serverPort, + " "); + logger.info("╠══════════════════════════════════════════════════════════════╣"); + logger.info("║ Monitor: {}s interval{}{} ║", + config.getMinecraft().getCheckIntervalSeconds(), + " ", + ""); + logger.info("║ Auto-Execute: {}{} ║", + config.getAi().getDeepseek().getAutoExecute().isEnabled() ? "enabled" : "disabled", + " "); + logger.info("║ Auto-Heal: {}{} ║", + config.getSecurity().getSelfHealing().isEnabled() ? "enabled" : "disabled", + " "); + logger.info("╚══════════════════════════════════════════════════════════════╝"); + logger.info(""); + logger.info("Type 'help' in the shell for available commands."); + logger.info("Access the web console at http://localhost:{}/", serverPort); + logger.info(""); + } + + private void validateConfiguration() { + int warnings = 0; + + if (!deepSeekClient.isEnabled()) { + logger.warn("CONFIG: DeepSeek AI is disabled. Set DEEPSEEK_API_KEY to enable AI features."); + warnings++; + } + + String rconPassword = config.getMinecraft().getRcon().getPassword(); + if (rconPassword == null || rconPassword.isEmpty()) { + logger.warn("CONFIG: RCON password is not set. Set RCON_PASSWORD env var."); + warnings++; + } + + if (!config.getAlert().getEmail().getUsername().isEmpty()) { + if (config.getAlert().getEmail().getTo() == null || config.getAlert().getEmail().getTo().isEmpty()) { + logger.warn("CONFIG: Email alert enabled but no recipients configured."); + warnings++; + } + } + + if (config.getSecurity().getHostEnforcement().isDryRun()) { + logger.info("CONFIG: Host enforcement is in dry-run mode (safe for first deployment)."); + } + if (config.getSecurity().getSelfHealing().isDryRun()) { + logger.info("CONFIG: Self-healing is in dry-run mode (safe for first deployment)."); + } + + if (warnings > 0) { + logger.info("Configuration check complete: {} warning(s) found.", warnings); + } else { + logger.info("Configuration validated successfully."); } } private void startMonitoring() { int intervalSeconds = config.getMinecraft().getCheckIntervalSeconds(); - + scheduler.scheduleAtFixedRate(this::checkProcess, 0, intervalSeconds, TimeUnit.SECONDS); scheduler.scheduleAtFixedRate(this::collectAndAnalyzeMetrics, 5, intervalSeconds, TimeUnit.SECONDS); - + if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) { int analysisInterval = config.getAi().getDeepseek().getAnalysisIntervalSeconds(); scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, analysisInterval, analysisInterval, TimeUnit.SECONDS); } - - logger.info("监控已启动, 间隔: {} 秒", intervalSeconds); + + logger.info("Monitoring started with {}s interval", intervalSeconds); } private void checkProcess() { try { AlertEvent event = processMonitor.checkAndRestart(); if (event != null) { - logger.warn("Process check alert: {}", event.getMessage()); + logger.warn("Process alert: {}", event.getMessage()); handleAlert(event); } } catch (Exception e) { - logger.error("Error in process check: {}", e.getMessage()); + logger.error("Process check failed: {}", e.getMessage()); } } private void collectAndAnalyzeMetrics() { try { MetricsData data = resourceMonitor.collectMetrics(); - + deepSeekClient.addMetrics(data); - adaptiveThreshold.addMetric(data); anomalyDetector.addMetric(data); timeSeriesPredictor.addMetric(data); - + List resourceAlerts = resourceMonitor.checkThresholds(data); for (AlertEvent alert : resourceAlerts) { handleAlert(alert); } - + AlertEvent connectionAlert = connectionMonitor.checkConnectionFlood(); if (connectionAlert != null) { handleAlert(connectionAlert); } - + List logAlerts = logMonitor.analyzeLogs(); for (AlertEvent alert : logAlerts) { handleAlert(alert); } - + if (config.getAi().isEnabled() && config.getAi().isUseIsolationForest()) { AlertEvent aiAlert = anomalyDetector.detectAnomaly(data); if (aiAlert != null) { handleAlert(aiAlert); } } - + if (config.getAi().isEnabled() && config.getAi().isUsePrediction()) { TimeSeriesPredictor.PredictionResult prediction = timeSeriesPredictor.predict(); if (prediction != null && prediction.isWillBreachThreshold() && prediction.getConfidence() > 0.6) { - logger.warn("TPS Prediction: {} (confidence: {:.0f}%) - {}", - prediction.getPredictedValue(), + logger.warn("TPS Prediction: {} (confidence: {:.0f}%) - {}", + prediction.getPredictedValue(), prediction.getConfidence() * 100, prediction.getRecommendation()); } } - + List attackAlerts = attackDetector.analyzeThreats(); for (AlertEvent alert : attackAlerts) { handleAlert(alert); } - + } catch (Exception e) { - logger.error("Error in metrics collection: {}", e.getMessage()); + logger.error("Metrics collection failed: {}", e.getMessage()); } } @@ -185,90 +263,77 @@ public class ServerGuardService implements CommandLineRunner { if (!deepSeekClient.isEnabled()) { return; } - + try { - DeepSeekClient.AiAnalysisResult healthReport = deepSeekClient.getServerHealthReportAsync().get(); - - if (healthReport != null) { - logger.info("=== DeepSeek AI 健康报告 ==="); - logger.info("状态: {}", healthReport.getSeverity()); - logger.info("摘要: {}", healthReport.getDescription()); - + deepSeekClient.getServerHealthReportAsync().thenAccept(healthReport -> { + if (healthReport == null) { + return; + } + logger.info("=== DeepSeek AI Health Report ==="); + logger.info("Status: {}", healthReport.getSeverity()); + logger.info("Summary: {}", healthReport.getDescription()); + if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) { - for (String concern : healthReport.getRecommendedActions()) { - logger.warn("建议: {}", concern); + for (String action : healthReport.getRecommendedActions()) { + logger.warn("Recommendation: {}", action); } } - } + }); } catch (Exception e) { - logger.error("DeepSeek 分析错误: {}", e.getMessage()); + logger.error("DeepSeek analysis failed: {}", e.getMessage()); } } private void handleAlert(AlertEvent alert) { deepSeekClient.addAlert(alert); - - logger.warn("告警 [{}]: {} (置信度: {:.0f}%)", + + logger.warn("Alert [{}]: {} (confidence: {:.0f}%)", alert.getType(), alert.getMessage(), alert.getConfidence() * 100); - + if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) { deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> { if (analysis != null) { - logger.info(" [DeepSeek AI 分析]"); - logger.info(" 严重程度: {}", analysis.getSeverity()); - logger.info(" 原因分析: {}", analysis.getRootCause()); - + logger.info(" [DeepSeek AI Analysis]"); + logger.info(" Severity: {}", analysis.getSeverity()); + logger.info(" Root Cause: {}", analysis.getRootCause()); + if (!analysis.getRecommendedActions().isEmpty()) { - logger.info(" 建议操作:"); + logger.info(" Recommended Actions:"); for (String action : analysis.getRecommendedActions()) { logger.info(" - {}", action); } } - + alert.setRootCause(analysis.getRootCause()); alert.setSuggestedAction(String.join("; ", analysis.getRecommendedActions())); - + autoExecutor.executeAutoAction(analysis, alert); } }); } else { if (alert.getRootCause() != null) { - logger.info(" 原因分析: {}", alert.getRootCause()); + logger.info(" Root Cause: {}", alert.getRootCause()); } if (alert.getSuggestedAction() != null) { - logger.info(" 建议操作: {}", alert.getSuggestedAction()); + logger.info(" Suggested Action: {}", alert.getSuggestedAction()); } } - + emailAlertService.sendAlert(alert); - - if (alert.getType().name().contains("CONNECTION_FLOOD") || + + if (alert.getType().name().contains("CONNECTION_FLOOD") || alert.getType().name().contains("LOG_ATTACK")) { if (alert.getConfidence() > 0.8) { - logger.info("高置信度攻击告警 - 触发自动防御"); + logger.info("High confidence attack alert — triggering auto defense"); } } } - private void executeAutoAction(DeepSeekClient.AiAnalysisResult analysis, AlertEvent alert) { - String autoAction = analysis.getAutoAction(); - - if (autoAction == null || autoAction.isEmpty() || autoAction.equals("无") || autoAction.equals("none")) { - return; - } - - logger.info(" [DeepSeek] 执行自动操作: {}", autoAction); - - if (autoAction.contains("封禁IP") || autoAction.contains("ban")) { - logger.info(" 自动封禁IP已触发"); - } else if (autoAction.contains("限制连接")) { - logger.info(" 自动连接限制已触发"); - } else if (autoAction.contains("重启")) { - logger.info(" 建议重启服务器"); - } + public boolean isRunning() { + return running.get(); } public void stop() { - running = false; + running.set(false); } } diff --git a/src/main/java/com/aluer/web/DashboardController.java b/src/main/java/com/aluer/web/DashboardController.java index cef4b4c..6ef9ca5 100644 --- a/src/main/java/com/aluer/web/DashboardController.java +++ b/src/main/java/com/aluer/web/DashboardController.java @@ -4,6 +4,8 @@ import com.aluer.ai.DeepSeekClient; import com.aluer.audit.SecurityAuditService; import com.aluer.backup.BackupService; import com.aluer.config.ServerGuardConfig; +import com.aluer.notification.AttackReportService; +import com.aluer.notification.WebhookService; import com.aluer.punishment.PunishmentService; import com.aluer.profiler.PerformanceProfiler; import com.aluer.security.NetworkThreatFusionService; @@ -28,6 +30,9 @@ public class DashboardController { private final DeepSeekClient deepSeekClient; private final ServerGuardConfig config; private final NetworkThreatFusionService networkThreatFusionService; + private final HealthService healthService; + private final AttackReportService attackReportService; + private final WebhookService webhookService; public DashboardController( RconClient rconClient, @@ -38,7 +43,10 @@ public class DashboardController { WorldManagementService worldService, DeepSeekClient deepSeekClient, ServerGuardConfig config, - NetworkThreatFusionService networkThreatFusionService) { + NetworkThreatFusionService networkThreatFusionService, + HealthService healthService, + AttackReportService attackReportService, + WebhookService webhookService) { this.rconClient = rconClient; this.profiler = profiler; this.backupService = backupService; @@ -48,6 +56,9 @@ public class DashboardController { this.deepSeekClient = deepSeekClient; this.config = config; this.networkThreatFusionService = networkThreatFusionService; + this.healthService = healthService; + this.attackReportService = attackReportService; + this.webhookService = webhookService; } @GetMapping("/status") @@ -166,4 +177,49 @@ public class DashboardController { status.put("enabled", true); return status; } + + @GetMapping("/health") + public Map getHealth() { + return healthService.getHealthReport(); + } + + @GetMapping("/health/live") + public Map getLiveness() { + return healthService.getLiveness(); + } + + @GetMapping("/health/ready") + public Map getReadiness() { + return healthService.getReadiness(); + } + + @GetMapping("/attacks/recent") + public Map getRecentAttacks(@RequestParam(defaultValue = "20") int limit) { + Map response = new HashMap<>(); + response.put("count", Math.min(limit, attackReportService.getRecentAttacks(Integer.MAX_VALUE).size())); + response.put("items", attackReportService.getRecentAttacks(limit)); + return response; + } + + @PostMapping("/attacks/report") + public Map generateAttackReport() { + Map response = new HashMap<>(); + try { + java.io.File report = attackReportService.saveHtmlReport(); + response.put("status", "generated"); + response.put("path", report.getAbsolutePath()); + } catch (Exception e) { + response.put("status", "failed"); + response.put("error", e.getMessage()); + } + return response; + } + + @PostMapping("/webhook/test") + public Map testWebhook() { + Map response = new HashMap<>(); + webhookService.sendCustomMessage("Aluer ServerGuard Test", "Webhook integration test successful.", "info"); + response.put("status", "sent"); + return response; + } } diff --git a/src/main/java/com/aluer/web/HealthService.java b/src/main/java/com/aluer/web/HealthService.java new file mode 100644 index 0000000..c8e92d5 --- /dev/null +++ b/src/main/java/com/aluer/web/HealthService.java @@ -0,0 +1,129 @@ +package com.aluer.web; + +import com.aluer.ai.DeepSeekClient; +import com.aluer.config.ServerGuardConfig; +import com.aluer.service.RconClient; +import org.springframework.stereotype.Service; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class HealthService { + + private final ServerGuardConfig config; + private final DeepSeekClient deepSeekClient; + private final RconClient rconClient; + private final OperatingSystemMXBean osBean; + private final MemoryMXBean memoryBean; + + public HealthService(ServerGuardConfig config, + DeepSeekClient deepSeekClient, + RconClient rconClient) { + this.config = config; + this.deepSeekClient = deepSeekClient; + this.rconClient = rconClient; + this.osBean = ManagementFactory.getOperatingSystemMXBean(); + this.memoryBean = ManagementFactory.getMemoryMXBean(); + } + + public Map getHealthReport() { + Map report = new LinkedHashMap<>(); + report.put("status", computeOverallStatus()); + report.put("timestamp", System.currentTimeMillis()); + report.put("uptimeSeconds", ManagementFactory.getRuntimeMXBean().getUptime() / 1000); + + report.put("components", buildComponentStatus()); + report.put("system", buildSystemInfo()); + report.put("version", "1.0.0"); + + return report; + } + + public Map getLiveness() { + Map result = new LinkedHashMap<>(); + result.put("status", "UP"); + result.put("timestamp", System.currentTimeMillis()); + return result; + } + + public Map getReadiness() { + Map result = new LinkedHashMap<>(); + boolean rconOk = rconClient.isConnected() || config.getMinecraft().getRcon().isEnabled(); + result.put("status", rconOk ? "READY" : "NOT_READY"); + result.put("rcon", rconOk ? "configured" : "unavailable"); + result.put("timestamp", System.currentTimeMillis()); + return result; + } + + private String computeOverallStatus() { + if (!rconClient.isConnected() && config.getMinecraft().getRcon().isEnabled()) { + return "DEGRADED"; + } + return "HEALTHY"; + } + + private Map buildComponentStatus() { + Map components = new LinkedHashMap<>(); + + Map rcon = new LinkedHashMap<>(); + rcon.put("status", rconClient.isConnected() ? "UP" : "DOWN"); + rcon.put("host", config.getMinecraft().getRcon().getHost() + ":" + config.getMinecraft().getRcon().getPort()); + components.put("rcon", rcon); + + Map ai = new LinkedHashMap<>(); + ai.put("status", deepSeekClient.isEnabled() ? "UP" : "DISABLED"); + ai.put("model", config.getAi().getDeepseek().getModel()); + components.put("deepseek-ai", ai); + + Map email = new LinkedHashMap<>(); + boolean emailConfigured = !config.getAlert().getEmail().getUsername().isEmpty(); + email.put("status", emailConfigured ? "UP" : "DISABLED"); + components.put("email-alert", email); + + Map security = new LinkedHashMap<>(); + security.put("status", config.getSecurity().isEnabled() ? "UP" : "DISABLED"); + security.put("kernelEnabled", config.getSecurity().getKernel().isEnabled()); + security.put("autonomyEnabled", config.getSecurity().getAutonomy().isEnabled()); + components.put("security", security); + + Map selfHealing = new LinkedHashMap<>(); + selfHealing.put("status", config.getSecurity().getSelfHealing().isEnabled() ? "UP" : "DISABLED"); + selfHealing.put("dryRun", config.getSecurity().getSelfHealing().isDryRun()); + components.put("self-healing", selfHealing); + + return components; + } + + private Map buildSystemInfo() { + Map system = new LinkedHashMap<>(); + system.put("javaVersion", System.getProperty("java.version")); + system.put("availableProcessors", Runtime.getRuntime().availableProcessors()); + + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long maxMemory = Runtime.getRuntime().maxMemory(); + long usedMemory = totalMemory - freeMemory; + + Map memory = new LinkedHashMap<>(); + memory.put("usedMB", usedMemory / (1024 * 1024)); + memory.put("freeMB", freeMemory / (1024 * 1024)); + memory.put("totalMB", totalMemory / (1024 * 1024)); + memory.put("maxMB", maxMemory / (1024 * 1024)); + memory.put("usagePercent", Math.round((double) usedMemory / maxMemory * 1000) / 10.0); + system.put("memory", memory); + + if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) { + system.put("systemCpuLoad", Math.round(sunOsBean.getCpuLoad() * 1000) / 10.0); + system.put("processCpuLoad", Math.round(sunOsBean.getProcessCpuLoad() * 1000) / 10.0); + } + + system.put("heapUsedMB", memoryBean.getHeapMemoryUsage().getUsed() / (1024 * 1024)); + system.put("heapMaxMB", memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024)); + + return system; + } +} diff --git a/src/main/java/com/aluer/web/RequestLoggingFilter.java b/src/main/java/com/aluer/web/RequestLoggingFilter.java new file mode 100644 index 0000000..29f9959 --- /dev/null +++ b/src/main/java/com/aluer/web/RequestLoggingFilter.java @@ -0,0 +1,43 @@ +package com.aluer.web; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE + 10) +public class RequestLoggingFilter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + long start = System.currentTimeMillis(); + + try { + chain.doFilter(request, response); + } finally { + long duration = System.currentTimeMillis() - start; + HttpServletResponse httpResponse = (HttpServletResponse) response; + int status = httpResponse.getStatus(); + + if (status >= 400 || duration > 1000) { + logger.debug("{} {} → {} ({}ms)", httpRequest.getMethod(), httpRequest.getRequestURI(), status, duration); + } + + if (status >= 500) { + logger.warn("Server error: {} {} → {} ({}ms)", httpRequest.getMethod(), httpRequest.getRequestURI(), status, duration); + } + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ef43fe6..abb085a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -165,6 +165,14 @@ serverguard: require-engine-handshake: true handshake-ttl-seconds: 30 + webhook: + enabled: false + discord-url: ${ALUER_WEBHOOK_DISCORD:} + slack-url: ${ALUER_WEBHOOK_SLACK:} + + report: + dir: ./reports + server: port: 8080