From a0ee37da24147d5d420c5f6d65832ac54dcaad0e Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Fri, 3 Apr 2026 01:36:24 +0200 Subject: [PATCH] Upload via Git Manager GUI --- statuspulse.php | 654 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 592 insertions(+), 62 deletions(-) diff --git a/statuspulse.php b/statuspulse.php index 472f5e2..fcb78bf 100644 --- a/statuspulse.php +++ b/statuspulse.php @@ -3,7 +3,7 @@ Plugin Name: StatusPulse Plugin URI:https://git.viper.ipv64.net/M_Viper/StatusPulse Description: Moderne WordPress-Admin-Seite für StatusAPI-Konfiguration, Attack-API-Key und Proxy-Tests. -Version: 1.0.0 +Version: 1.0.1 Author: M_Viper Author URI: https://m-viper.de Requires at least: 6.8 @@ -290,7 +290,118 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return ''; } - return untrailingslashit(esc_url_raw($url)); + $url = untrailingslashit(esc_url_raw($url)); + + // Falls aus anderen Plugins ein Endpoint eingetragen wurde, + // auf reine Basis-URL zurückführen. + $known_suffixes = array( + '/broadcast', + '/network/attack', + '/antibot/security-log', + '/health', + ); + foreach ($known_suffixes as $suffix) { + if (substr($url, -strlen($suffix)) === $suffix) { + $url = substr($url, 0, -strlen($suffix)); + break; + } + } + + return untrailingslashit($url); + } + + private static function get_pulsecast_api_url() { + $settings = get_option('pulsecast_settings', array()); + if (!is_array($settings)) { + return ''; + } + + $protocol = isset($settings['api_protocol']) ? strtolower(trim((string) $settings['api_protocol'])) : 'http'; + if ($protocol !== 'http' && $protocol !== 'https') { + $protocol = 'http'; + } + + $host = isset($settings['api_host']) ? trim((string) $settings['api_host']) : ''; + if ($host === '') { + return ''; + } + + $port = isset($settings['api_port']) ? trim((string) $settings['api_port']) : ''; + $path = isset($settings['api_path']) ? trim((string) $settings['api_path']) : '/broadcast'; + if ($path === '') { + $path = '/broadcast'; + } + if (substr($path, 0, 1) !== '/') { + $path = '/' . $path; + } + + $url = $protocol . '://' . $host; + if ($port !== '' && !(($protocol === 'http' && $port === '80') || ($protocol === 'https' && $port === '443'))) { + $url .= ':' . $port; + } + // Wichtig: exakt wie PulseCast verwenden (inkl. konfiguriertem Pfad). + // So verhalten wir uns identisch zum funktionierenden Plugin. + $url .= $path; + return esc_url_raw($url); + } + + private static function get_candidate_base_urls($base_url) { + $candidates = array(); + $add = function($url) use (&$candidates) { + $u = trim((string) $url); + if ($u !== '' && !in_array($u, $candidates, true)) { + $candidates[] = $u; + } + }; + + $add_base_variants = function($url) use ($add) { + $u = trim((string) $url); + if ($u === '') { + return; + } + + // Wenn nur Domain ohne Schema gespeichert wurde: wie in den anderen Plugins auf http normalisieren. + if (strpos($u, 'http://') !== 0 && strpos($u, 'https://') !== 0) { + $u = 'http://' . $u; + } + + // Primär: exakt die konfigurierte URL testen. + $root = untrailingslashit($u); + $add($root); + + // Direkt danach Port 9191 testen – falls NPM nicht korrekt routet, finden + // wir den StatusAPI-Server so viel schneller (vor /health etc.) + $parts = wp_parse_url($u); + if (is_array($parts) && !empty($parts['host']) && empty($parts['port'])) { + $host_9191 = 'http://' . $parts['host'] . ':9191'; + $add($host_9191); + $add($host_9191 . '/'); + $add($host_9191 . '/health'); + } + + $add($root . '/'); + $add($root . '/health'); + + // Falls https gesetzt ist, auch http testen (häufiger NPM/SSL Sonderfall). + if (strpos($root, 'https://') === 0) { + $http_root = 'http://' . substr($root, 8); + $add($http_root); + $add($http_root . '/'); + $add($http_root . '/health'); + } + }; + + $primary = trim((string) $base_url); + if ($primary !== '') { + $add_base_variants($primary); + } + + $pulsecast_url = self::get_pulsecast_api_url(); + if ($pulsecast_url !== '') { + $add_base_variants($pulsecast_url); + } + + return $candidates; } private static function get_options() { @@ -404,13 +515,22 @@ if (!class_exists('StatusAPI_Backend_Helper')) { $live_status = self::probe_statusapi($statusapi_base_url); $state = self::get_state(); $notice = self::build_notice(); - $status_endpoint = $statusapi_base_url !== '' ? $statusapi_base_url : 'http://dein-proxy:9191'; + $candidate_urls_for_ui = self::get_candidate_base_urls($statusapi_base_url); + $status_endpoint = !empty($candidate_urls_for_ui) + ? $candidate_urls_for_ui[0] + : ''; $attack_endpoint = $status_endpoint . '/network/attack'; $status_json_snippet = "Status JSON: {$status_endpoint}"; $network_guard_snippet = "networkinfo.attack.api_key={$attack_api_key}\nnetworkinfo.attack.source={$attack_source}"; $curl_examples = self::build_curl_examples($status_endpoint, $attack_endpoint, $attack_api_key, $attack_source); $refresh_nonce = wp_create_nonce('statusapi_backend_helper_refresh'); + $debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array(); + $debug_raw_data = isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : ''; + $debug_error = isset($debug_data['error']) ? (string) $debug_data['error'] : ''; + $debug_url_used = isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : ''; + $debug_attempts = isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array(); + echo '
'; self::render_styles(); self::render_scripts($refresh_nonce); @@ -473,13 +593,37 @@ if (!class_exists('StatusAPI_Backend_Helper')) { self::render_metric_card('statusapi-metric-memory', 'RAM Nutzung', $live_status['metrics']['memory'], $live_status['metrics']['memory_meta']); echo '
'; + echo '
'; + echo '
'; + echo '

🔍 DEBUG: API Response (klicken zum Ein-/Ausblenden)

'; + echo '
'; + echo ''; + echo '
'; + echo '
'; echo '
'; echo '

Konfiguration

Die Werte werden lokal in WordPress gespeichert und als Snippets für deine Proxy-Dateien angezeigt.

'; echo '
'; settings_fields('statusapi_backend_helper_group'); echo '
'; - self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'z. B. http://127.0.0.1:9191'); + self::render_input_field('StatusAPI Basis-URL', self::OPTION_KEY . '[statusapi_base_url]', $statusapi_base_url, 'Bei NPM: https://deine-domain.tld (ohne :9191, ohne /broadcast)'); self::render_input_field('Attack API-Key', self::OPTION_KEY . '[attack_api_key]', $attack_api_key, 'Entspricht networkinfo.attack.api_key in network-guard.properties.'); self::render_input_field('Attack Source', self::OPTION_KEY . '[attack_source]', $attack_source, 'Wird in Testmeldungen als Quelle angezeigt.'); echo '
'; @@ -513,6 +657,7 @@ if (!class_exists('StatusAPI_Backend_Helper')) { echo '
'; echo '
'; echo ''; + echo ''; } public static function render_messages_page() { @@ -690,6 +835,160 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return "GET {$status_endpoint}\n\nPOST {$attack_endpoint}\nHeader: X-API-Key: {$attack_api_key}\nBody: {$payload}"; } + private static function is_supported_api_payload($decoded) { + if (!is_array($decoded)) { + return false; + } + + $is_statusapi = isset($decoded['network']) && is_array($decoded['network']); + $has_players_array = isset($decoded['players']) && is_array($decoded['players']); + $has_online_flag = isset($decoded['online']) + && (is_bool($decoded['online']) || $decoded['online'] === 0 || $decoded['online'] === 1 || $decoded['online'] === '0' || $decoded['online'] === '1' || $decoded['online'] === 'true' || $decoded['online'] === 'false'); + $is_bungeecord = $has_online_flag && isset($decoded['players']) && is_array($decoded['players']); + + // Wie in mc-player-history: players[] gilt bereits als verwertbares StatusAPI-JSON. + return $is_statusapi || $is_bungeecord || $has_players_array; + } + + private static function should_try_stream_fallback($error_message) { + $msg = strtolower((string) $error_message); + return strpos($msg, 'curl error 35') !== false + || strpos($msg, 'tlsv1 unrecognized name') !== false + || strpos($msg, 'ssl routines') !== false; + } + + private static function stream_http_request($url, $method, $headers, $body, $timeout) { + $parts = wp_parse_url($url); + if (!is_array($parts) || empty($parts['host'])) { + return new WP_Error('statuspulse_stream_bad_url', 'Ungueltige URL fuer Stream-Fallback.'); + } + + $scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : 'http'; + $host = (string) $parts['host']; + $port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80); + $path = isset($parts['path']) ? (string) $parts['path'] : '/'; + if ($path === '') { + $path = '/'; + } + if (!empty($parts['query'])) { + $path .= '?' . $parts['query']; + } + + $transport = $scheme === 'https' ? 'ssl://' : 'tcp://'; + $context_options = array(); + if ($scheme === 'https') { + $context_options['ssl'] = array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + // Workaround fuer Umgebungen mit problematischem SNI/TLS Handshake via cURL. + 'SNI_enabled' => false, + ); + } + $context = stream_context_create($context_options); + + $errno = 0; + $errstr = ''; + $fp = @stream_socket_client($transport . $host . ':' . $port, $errno, $errstr, (float) $timeout, STREAM_CLIENT_CONNECT, $context); + if (!$fp) { + return new WP_Error('statuspulse_stream_connect_failed', 'Stream-Verbindung fehlgeschlagen: ' . $errstr); + } + + stream_set_timeout($fp, (int) max(1, (int) $timeout)); + + $request_headers = array( + 'Host' => $host, + 'Connection' => 'close', + 'User-Agent' => 'StatusPulse/1.0', + 'Accept' => 'application/json, */*', + ); + foreach ((array) $headers as $hk => $hv) { + $request_headers[(string) $hk] = (string) $hv; + } + + $payload = (string) $body; + if ($payload !== '') { + $request_headers['Content-Length'] = (string) strlen($payload); + } + + $raw = strtoupper($method) . ' ' . $path . " HTTP/1.1\r\n"; + foreach ($request_headers as $hk => $hv) { + $raw .= $hk . ': ' . $hv . "\r\n"; + } + $raw .= "\r\n"; + if ($payload !== '') { + $raw .= $payload; + } + + fwrite($fp, $raw); + + $response_raw = ''; + while (!feof($fp)) { + $chunk = fread($fp, 8192); + if ($chunk === false) { + break; + } + $response_raw .= $chunk; + } + fclose($fp); + + if ($response_raw === '') { + return new WP_Error('statuspulse_stream_empty', 'Leere Antwort vom Stream-Fallback.'); + } + + $parts_resp = explode("\r\n\r\n", $response_raw, 2); + $head = isset($parts_resp[0]) ? $parts_resp[0] : ''; + $resp_body = isset($parts_resp[1]) ? $parts_resp[1] : ''; + + $status_code = 0; + $head_lines = explode("\r\n", $head); + if (!empty($head_lines)) { + if (preg_match('#HTTP/\d\.\d\s+(\d{3})#', (string) $head_lines[0], $m)) { + $status_code = (int) $m[1]; + } + } + + return array( + 'code' => $status_code, + 'body' => $resp_body, + 'error' => '', + 'via' => 'stream', + ); + } + + private static function perform_request_with_fallback($url, $method, $headers = array(), $body = '', $timeout = 5) { + $args = array( + 'timeout' => $timeout, + 'sslverify' => false, + 'headers' => is_array($headers) ? $headers : array(), + ); + if (strtoupper($method) !== 'GET') { + $args['body'] = (string) $body; + } + + $response = wp_remote_request($url, array_merge($args, array('method' => strtoupper($method)))); + if (!is_wp_error($response)) { + return array( + 'code' => (int) wp_remote_retrieve_response_code($response), + 'body' => (string) wp_remote_retrieve_body($response), + 'error' => '', + 'via' => 'wp', + ); + } + + $error_message = $response->get_error_message(); + if (!self::should_try_stream_fallback($error_message)) { + return array('code' => 0, 'body' => '', 'error' => $error_message, 'via' => 'wp'); + } + + $fallback = self::stream_http_request($url, $method, $headers, $body, $timeout); + if (is_wp_error($fallback)) { + return array('code' => 0, 'body' => '', 'error' => $error_message . ' | Stream-Fallback: ' . $fallback->get_error_message(), 'via' => 'wp+stream'); + } + + return $fallback; + } + private static function probe_statusapi($base_url) { $state = self::get_state(); $previous_status = isset($state['last_status']) ? (string) $state['last_status'] : 'unknown'; @@ -697,43 +996,113 @@ if (!class_exists('StatusAPI_Backend_Helper')) { $previous_memory_high = !empty($state['last_memory_high']); $previous_player_high = !empty($state['last_player_high']); - if ($base_url === '') { + $candidate_urls = self::get_candidate_base_urls($base_url); + if (empty($candidate_urls)) { return array( 'status' => 'unknown', 'label' => 'Nicht konfiguriert', - 'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen.', + 'meta' => 'Bitte zuerst eine StatusAPI Basis-URL eintragen (oder PulseCast konfigurieren).', 'http_code' => 0, 'checked_at_human' => self::format_timestamp($state['last_check_at']), 'metrics' => self::empty_metrics(), + 'debug' => array( + 'url_used' => '', + 'attempts' => array(), + 'error' => 'Keine Basis-URL gesetzt.', + 'raw_data' => '', + ), ); } - $response = wp_remote_get($base_url, array('timeout' => 5)); + $response_url = $candidate_urls[0]; + $response = null; + $last_error = ''; + $is_valid_response = false; + $is_reachable = false; + $reachable_code = 0; + $attempts = array(); + $raw_fallback = ''; + + foreach ($candidate_urls as $try_url) { + $response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 5); + + if (!empty($response['error'])) { + $last_error = $response['error']; + $attempts[] = $try_url . ' -> ERROR: ' . $response['error']; + continue; + } + + $code_tmp = (int) $response['code']; + $body_tmp = (string) $response['body']; + $decoded_tmp = json_decode($body_tmp, true); + + if ($code_tmp > 0 && $code_tmp < 500 && !$is_reachable) { + $is_reachable = true; + $reachable_code = $code_tmp; + $response_url = $try_url; + } + + if ($code_tmp >= 200 && $code_tmp < 300 && self::is_supported_api_payload($decoded_tmp)) { + $response_url = $try_url; + $is_valid_response = true; + $last_error = ''; + $attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (StatusAPI JSON)'; + break; + } + + if ($code_tmp >= 200 && $code_tmp < 300 && !self::is_supported_api_payload($decoded_tmp)) { + $attempts[] = $try_url . ' -> HTTP ' . $code_tmp . ' (kein StatusAPI JSON)'; + if ($raw_fallback === '') { + $raw_fallback = $body_tmp; + } + if (is_string($body_tmp) && stripos($body_tmp, 'successfully started the Nginx Proxy Manager') !== false) { + // NPM-Domain ist nicht korrekt auf Port 9191 geroutet + $parsed_npm = wp_parse_url($try_url); + $npm_host = is_array($parsed_npm) && !empty($parsed_npm['host']) ? $parsed_npm['host'] : ''; + $last_error = 'Nginx Proxy Manager Default-Seite erkannt – Proxy-Host für diese Domain fehlt.' + . ($npm_host !== '' ? ' Trage stattdessen direkt ein: http://' . $npm_host . ':9191' : ''); + } else { + $last_error = 'Antwort ist kein StatusAPI-JSON.'; + } + } else { + $attempts[] = $try_url . ' -> HTTP ' . $code_tmp; + $last_error = 'HTTP ' . $code_tmp; + } + } + $now = time(); - if (is_wp_error($response)) { + if (!$is_valid_response) { $state['last_check_at'] = $now; - $state['last_http_code'] = 0; + $state['last_http_code'] = $is_reachable ? $reachable_code : 0; $state['last_status'] = 'error'; - $state['last_error'] = $response->get_error_message(); + $state['last_error'] = $last_error !== '' ? $last_error : 'Ungueltige Antwort vom Proxy'; if ($previous_status !== 'error') { - self::push_message($state, 'error', 'Proxy offline', 'StatusAPI ist nicht erreichbar: ' . $response->get_error_message()); + self::push_message($state, 'error', 'Proxy-Fehlkonfiguration', 'StatusAPI liefert keine gueltigen JSON-Daten. Pruefe Nginx Proxy Host fuer die Domain.'); } self::save_state($state); + error_log('StatusPulse: Fehler fuer ' . implode(', ', $candidate_urls) . ': ' . $state['last_error']); + return array( 'status' => 'error', - 'label' => 'Offline', - 'meta' => $response->get_error_message(), + 'label' => 'Fehlkonfiguration', + 'meta' => $state['last_error'], 'http_code' => 0, 'checked_at_human' => self::format_timestamp($now), 'metrics' => self::empty_metrics(), + 'debug' => array( + 'url_used' => $response_url, + 'attempts' => $attempts, + 'error' => $state['last_error'], + 'raw_data' => $raw_fallback, + ), ); } - $code = (int) wp_remote_retrieve_response_code($response); + $code = (int) $response['code']; $ok = $code >= 200 && $code < 300; - $body = wp_remote_retrieve_body($response); + $body = (string) $response['body']; $decoded = json_decode($body, true); $state['last_check_at'] = $now; $state['last_http_code'] = $code; @@ -800,10 +1169,16 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return array( 'status' => $ok ? 'ok' : 'error', 'label' => $ok ? 'Online' : 'Fehler', - 'meta' => $ok ? 'Antwort von ' . untrailingslashit($base_url) : 'HTTP ' . $code, + 'meta' => $ok ? 'Antwort von ' . untrailingslashit($response_url) : 'HTTP ' . $code, 'http_code' => $code, 'checked_at_human' => self::format_timestamp($now), 'metrics' => $ok ? self::extract_metrics($decoded) : self::empty_metrics(), + 'debug' => array( + 'url_used' => $response_url, + 'attempts' => $attempts, + 'error' => $ok ? '' : 'HTTP ' . $code, + 'raw_data' => $body, + ), ); } @@ -812,28 +1187,79 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return self::empty_metrics(); } - $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array(); - $players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array(); - $memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array(); + $players_list_count = self::count_players_fallback($decoded); - $online_players = self::array_value($players, 'online', self::count_players_fallback($decoded)); - $max_players = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a')); - $occupancy = self::array_value($players, 'occupancy_percent', null); - $uptime_human = self::array_value($network, 'uptime_human', 'n/a'); - $uptime_seconds = self::array_value($network, 'uptime_seconds', null); - $memory_percent = self::array_value($memory, 'usage_percent', 'n/a'); - $memory_used = self::array_value($memory, 'used_mb', 'n/a'); - $memory_max = self::array_value($memory, 'max_mb', 'n/a'); + // StatusAPI-Format + $network = isset($decoded['network']) && is_array($decoded['network']) ? $decoded['network'] : array(); + if (!empty($network)) { + $players = isset($network['players']) && is_array($network['players']) ? $network['players'] : array(); + $memory = isset($network['memory']) && is_array($network['memory']) ? $network['memory'] : array(); + + // Wie in MC-Player-History: players[] als primäre Quelle bevorzugen. + $online_players = $players_list_count !== 'n/a' + ? $players_list_count + : self::array_value($players, 'online', 'n/a'); + + $max_players_raw = self::array_value($players, 'max', self::array_value($decoded, 'max_players', 'n/a')); + $max_players = self::normalize_max_players($max_players_raw); + $occupancy = self::array_value($players, 'occupancy_percent', null); + $uptime_human = self::array_value($network, 'uptime_human', 'n/a'); + $uptime_seconds = self::array_value($network, 'uptime_seconds', null); + $memory_percent = self::array_value($memory, 'usage_percent', null); + $memory_used = self::array_value($memory, 'used_mb', 'n/a'); + $memory_max = self::array_value($memory, 'max_mb', 'n/a'); + + $memory_value = 'n/a'; + if ($memory_percent !== null && $memory_percent !== 'n/a' && $memory_percent !== '') { + $memory_value = ((string) $memory_percent) . '%'; + } + + return array( + 'online_players' => (string) $online_players, + 'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler', + 'max_players' => (string) $max_players, + 'max_meta' => 'Player-Limit laut Proxy', + 'uptime' => (string) $uptime_human, + 'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten', + 'memory' => $memory_value, + 'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB', + ); + } + + // BungeeCord-Format + $is_bungeecord = isset($decoded['players']) && is_array($decoded['players']) && isset($decoded['online']); + if ($is_bungeecord) { + return self::extract_metrics_bungeecord($decoded); + } + + return self::empty_metrics(); + } + + private static function extract_metrics_bungeecord($decoded) { + if (!is_array($decoded)) { + return self::empty_metrics(); + } + + $players_array = isset($decoded['players']) && is_array($decoded['players']) ? $decoded['players'] : array(); + $online_raw = self::array_value($decoded, 'online', null); + $is_online = $online_raw === true || $online_raw === 1 || $online_raw === '1' || $online_raw === 'true'; + $online_count = $is_online ? count($players_array) : '0'; + + $max_players = self::array_value($decoded, 'max_players', 'n/a'); + if ($max_players === 'n/a' && !empty($players_array) && is_array($players_array[0]) && isset($players_array[0]['max'])) { + $max_players = (int) $players_array[0]['max']; + } + $max_players = self::normalize_max_players($max_players); return array( - 'online_players' => (string) $online_players, - 'online_meta' => $occupancy !== null ? 'Auslastung ' . $occupancy . '%' : 'Aktuell verbundene Spieler', + 'online_players' => (string) $online_count, + 'online_meta' => 'Spieler online (BungeeCord)', 'max_players' => (string) $max_players, - 'max_meta' => 'Player-Limit laut Proxy', - 'uptime' => (string) $uptime_human, - 'uptime_meta' => $uptime_seconds !== null ? ((string) $uptime_seconds) . ' Sekunden' : 'Keine Uptime-Daten', - 'memory' => ((string) $memory_percent) . '%', - 'memory_meta' => $memory_used . ' MB von ' . $memory_max . ' MB', + 'max_meta' => 'Max Spieler', + 'uptime' => 'n/a', + 'uptime_meta' => 'BungeeCord Format (nicht verfügbar)', + 'memory' => 'n/a', + 'memory_meta' => 'BungeeCord Format (nicht verfügbar)', ); } @@ -864,6 +1290,20 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return 'n/a'; } + private static function normalize_max_players($value) { + // -1 bedeutet: BungeeCord hat keinen Listener-Wert und kein globales Limit. + // Wird ab StatusAPI 4.1+ eigentlich nicht mehr auftreten da Java den Listener-Wert liefert. + if ($value === -1 || $value === '-1' || (is_numeric($value) && (int) $value === -1)) { + return '∞'; + } + + if (is_numeric($value) && (int) $value <= 0) { + return 'n/a'; + } + + return $value; + } + private static function format_timestamp($timestamp) { $timestamp = (int) $timestamp; if ($timestamp <= 0) { @@ -892,7 +1332,12 @@ if (!class_exists('StatusAPI_Backend_Helper')) { return array('type' => 'error', 'message' => 'Bitte zuerst einen Attack API-Key speichern.'); } if ($code === 'connection_failed') { - return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.'); + $state = self::get_state(); + $suffix = ''; + if (!empty($state['last_error'])) { + $suffix = ' Letzter Fehler: ' . (string) $state['last_error']; + } + return array('type' => 'error', 'message' => 'Verbindungstest fehlgeschlagen. Bitte Basis-URL und Erreichbarkeit prüfen.' . $suffix); } if ($code === 'attack_failed') { return array('type' => 'error', 'message' => 'Test Attack-Meldung konnte nicht gesendet werden. API-Key und Proxy-Endpunkt prüfen.'); @@ -918,13 +1363,16 @@ if (!class_exists('StatusAPI_Backend_Helper')) { } if (isset($_POST['test_connection'])) { - $ok = self::request_ok($base_url); + $probe = self::probe_statusapi($base_url); + $ok = isset($probe['status']) && $probe['status'] === 'ok'; $state = self::get_state(); self::push_message( $state, $ok ? 'success' : 'error', $ok ? 'Verbindungstest erfolgreich' : 'Verbindungstest fehlgeschlagen', - $ok ? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.' : 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich.' + $ok + ? 'Manueller Verbindungstest im WordPress-Backend war erfolgreich.' + : 'Manueller Verbindungstest im WordPress-Backend war nicht erfolgreich. ' . (isset($probe['meta']) ? (string) $probe['meta'] : '') ); self::save_state($state); self::redirect_with_notice($ok ? 'connection_ok' : 'connection_failed'); @@ -943,27 +1391,29 @@ if (!class_exists('StatusAPI_Backend_Helper')) { 'connectionsBlocked' => 1800, ); - $response = wp_remote_post($base_url . '/network/attack', array( - 'timeout' => 8, - 'headers' => array( + $response = self::perform_request_with_fallback( + $base_url . '/network/attack', + 'POST', + array( 'Content-Type' => 'application/json; charset=utf-8', 'X-API-Key' => $options['attack_api_key'], ), - 'body' => wp_json_encode($payload), - )); + wp_json_encode($payload), + 8 + ); - if (is_wp_error($response)) { + if (!empty($response['error'])) { $state = self::get_state(); $state['last_check_at'] = time(); $state['last_http_code'] = 0; $state['last_status'] = 'error'; - $state['last_error'] = $response->get_error_message(); - self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response->get_error_message()); + $state['last_error'] = $response['error']; + self::push_message($state, 'error', 'Test-Attack fehlgeschlagen', 'Konnte keine Test-Meldung senden: ' . $response['error']); self::save_state($state); self::redirect_with_notice('attack_failed'); } - $code = (int) wp_remote_retrieve_response_code($response); + $code = (int) $response['code']; $state = self::get_state(); $state['last_check_at'] = time(); $state['last_http_code'] = $code; @@ -1020,9 +1470,17 @@ if (!class_exists('StatusAPI_Backend_Helper')) { $live_status = self::probe_statusapi($options['statusapi_base_url']); $state = self::get_state(); + $debug_data = isset($live_status['debug']) && is_array($live_status['debug']) ? $live_status['debug'] : array(); + wp_send_json_success(array( 'refresh_state' => 'Aktualisiert ' . self::format_timestamp($state['last_check_at']), 'messages_html' => self::build_messages_markup($state), + 'debug' => array( + 'url_used' => isset($debug_data['url_used']) ? (string) $debug_data['url_used'] : '', + 'attempts' => isset($debug_data['attempts']) && is_array($debug_data['attempts']) ? $debug_data['attempts'] : array(), + 'error' => isset($debug_data['error']) ? (string) $debug_data['error'] : '', + 'raw_data' => isset($debug_data['raw_data']) ? (string) $debug_data['raw_data'] : '', + ), 'status_cards' => array( 'proxy' => array( 'value' => $live_status['label'], @@ -1067,52 +1525,108 @@ if (!class_exists('StatusAPI_Backend_Helper')) { } private static function request_ok($url) { - $response = wp_remote_get($url, array('timeout' => 8)); - if (is_wp_error($response)) { + $candidate_urls = self::get_candidate_base_urls($url); + if (empty($candidate_urls)) { return false; } - $code = (int) wp_remote_retrieve_response_code($response); - return $code >= 200 && $code < 300; + foreach ($candidate_urls as $try_url) { + $response = self::perform_request_with_fallback($try_url, 'GET', array(), '', 8); + + if (!empty($response['error'])) { + error_log('StatusPulse: request_ok() fehlgeschlagen fuer ' . $try_url . ' - ' . $response['error']); + continue; + } + + $code = (int) $response['code']; + $decoded = json_decode((string) $response['body'], true); + error_log('StatusPulse: request_ok() fuer ' . $try_url . ' -> HTTP ' . $code); + if ($code >= 200 && $code < 300 && self::is_supported_api_payload($decoded)) { + return true; + } + } + + return false; } private static function fetch_attacker_entries($base_url) { - if ($base_url === '') { + // Schritt 1: Arbeitsfähige Basis-URL ermitteln (erste die valide antwortet) + $candidate_urls = self::get_candidate_base_urls($base_url); + if (empty($candidate_urls)) { return array( 'entries' => array(), 'error' => 'StatusAPI Basis-URL ist nicht konfiguriert.', ); } - $endpoint = untrailingslashit($base_url) . '/antibot/security-log'; - $response = wp_remote_get($endpoint, array('timeout' => 8)); - if (is_wp_error($response)) { + // Finde die funktionierende Basis-URL (die echtes StatusAPI-JSON liefert) + $working_base = ''; + foreach ($candidate_urls as $try_url) { + // Entferne bekannte Pfad-Suffixe damit wir nur die Root-URL testen + $root_url = rtrim(preg_replace('#/(health|antibot/security-log|broadcast|network/attack)$#', '', $try_url), '/'); + if ($root_url === '') { + continue; + } + $probe = self::perform_request_with_fallback($root_url, 'GET', array(), '', 8); + if (empty($probe['error'])) { + $probe_code = (int) $probe['code']; + $probe_decoded = json_decode((string) $probe['body'], true); + if ($probe_code >= 200 && $probe_code < 300 && self::is_supported_api_payload($probe_decoded)) { + $working_base = $root_url; + break; + } + } + } + + if ($working_base === '') { return array( 'entries' => array(), - 'error' => 'Konnte Angriffsdaten nicht laden: ' . $response->get_error_message(), + 'error' => 'StatusAPI konnte nicht erreicht werden. Bitte Basis-URL und Erreichbarkeit prüfen.', ); } - $code = (int) wp_remote_retrieve_response_code($response); + // Schritt 2: Dedizierter Endpunkt /antibot/security-log abrufen + $security_log_url = rtrim($working_base, '/') . '/antibot/security-log'; + $response = self::perform_request_with_fallback($security_log_url, 'GET', array(), '', 8); + + if (!empty($response['error'])) { + return array( + 'entries' => array(), + 'error' => 'Konnte Security-Log nicht laden: ' . $response['error'], + ); + } + + $code = (int) $response['code']; if ($code < 200 || $code >= 300) { return array( 'entries' => array(), - 'error' => 'StatusAPI lieferte HTTP ' . $code . ' für /antibot/security-log.', + 'error' => 'StatusAPI /antibot/security-log lieferte HTTP ' . $code . '.', ); } - $body = wp_remote_retrieve_body($response); + $body = (string) $response['body']; $decoded = json_decode($body, true); - if (!is_array($decoded) || !isset($decoded['events']) || !is_array($decoded['events'])) { + if (!is_array($decoded)) { return array( 'entries' => array(), - 'error' => 'Antwort von /antibot/security-log ist ungültig.', + 'error' => 'Antwort vom Security-Log-Endpunkt ist kein gültiges JSON.', ); } + $entries = array(); + + // StatusAPI liefert { "success": true, "events": [...] } + if (isset($decoded['events']) && is_array($decoded['events'])) { + foreach (array_slice($decoded['events'], 0, 100) as $entry) { + if (is_array($entry)) { + $entries[] = $entry; + } + } + } + return array( - 'entries' => $decoded['events'], - 'error' => '', + 'entries' => $entries, + 'error' => count($entries) === 0 ? 'Keine Angriffsversuche im Log vorhanden.' : '', ); } @@ -1699,6 +2213,22 @@ if (!class_exists('StatusAPI_Backend_Helper')) { applyMetricCard("statusapi-metric-uptime", result.data.metric_cards.uptime); applyMetricCard("statusapi-metric-memory", result.data.metric_cards.memory); + // Debug-Panel live aktualisieren + if (result.data.debug) { + var dbg = result.data.debug; + var urlEl = document.getElementById("statuspulse-debug-url"); + var attEl = document.getElementById("statuspulse-debug-attempts"); + var rawEl = document.getElementById("statuspulse-debug-raw"); + var errEl = document.getElementById("statuspulse-debug-error"); + if (urlEl) urlEl.textContent = dbg.url_used || "(nicht gesetzt)"; + if (attEl) attEl.textContent = dbg.attempts && dbg.attempts.length ? dbg.attempts.join(" | ") : ""; + if (rawEl) rawEl.textContent = dbg.raw_data || "(keine Response)"; + if (errEl) { + errEl.textContent = dbg.error || ""; + errEl.style.display = dbg.error ? "block" : "none"; + } + } + var messageList = document.getElementById("statusapi-message-list"); if (messageList && typeof result.data.messages_html !== "undefined") { messageList.innerHTML = result.data.messages_html;