$headers */ function extract_http_status_code(array $headers): ?int { foreach ($headers as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#i', (string)$h, $m)) { return (int)$m[1]; } } return null; } /** * Decide if a HTTP status code means the service is reachable. * We treat 2xx/3xx as up and also 401/403 as up (service is there, but needs auth). */ function is_reachable_http_code(?int $code): bool { if ($code === null) return false; if ($code >= 200 && $code < 400) return true; if ($code === 401 || $code === 403) return true; return false; } /** * Perform a lightweight HTTP request (HEAD/GET) without following redirects. * @return array{ok:bool, code:int|null, ms:int|null, error:string|null} */ function check_http_request(string $url, string $method, float $timeoutSeconds = 1.6, ?string $extraHeaders = null): array { $start = microtime(true); $http_response_header = null; // ensure local scope, avoid stale values $code = null; $error = null; $headers = "User-Agent: ServerStatus/1.1\r\n"; if ($extraHeaders) { $headers .= $extraHeaders; if (!str_ends_with($headers, "\r\n")) { $headers .= "\r\n"; } } $ctx = stream_context_create([ 'http' => [ 'method' => $method, 'timeout' => $timeoutSeconds, 'ignore_errors' => true, 'follow_location' => 0, 'max_redirects' => 0, 'header' => $headers, ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); set_error_handler(static function (int $severity, string $message) use (&$error): bool { // capture warning as error string (e.g. connection refused, timeout, DNS fail) $error = $message; return true; }); $fp = @fopen($url, 'rb', false, $ctx); restore_error_handler(); if ($fp !== false) { // For GET we don't want to download; the Range header should keep it tiny. fclose($fp); } $ms = (int)round((microtime(true) - $start) * 1000); if (isset($http_response_header) && is_array($http_response_header)) { $code = extract_http_status_code($http_response_header); } $ok = is_reachable_http_code($code); // If we couldn't even open the stream, treat as hard failure, regardless of any weird headers. if ($fp === false) { $ok = false; if ($error === null) { $error = 'Verbindung fehlgeschlagen'; } } if ($code === null && $error === null) { $error = 'Unbekannter Fehler'; } return ['ok' => $ok, 'code' => $code, 'ms' => $ms, 'error' => $error]; } /** * HTTP check (no redirects). Try HEAD first, then a minimal GET fallback (some servers lie on HEAD). * @return array{ok:bool, code:int|null, ms:int|null, error:string|null} */ function check_http_head(string $url, float $timeoutSeconds = 1.6): array { $head = check_http_request($url, 'HEAD', $timeoutSeconds); if ($head['ok']) { return $head; } // If HEAD is not supported/misconfigured, a GET with Range is a cheap reachability check. $get = check_http_request($url, 'GET', $timeoutSeconds, "Range: bytes=0-0\r\n"); // prefer the GET result if it produced a code (more reliable) if ($get['code'] !== null) { return $get; } // otherwise fall back to the HEAD info return $head; } function build_server_status(array $targets): array { $allowedHosts = []; foreach ($targets as $t) { if (!empty($t['url'])) { $p = parse_url((string)$t['url']); if (is_array($p) && !empty($p['host'])) { $allowedHosts[] = strtolower((string)$p['host']); } } } $allowedHosts = array_values(array_unique($allowedHosts)); $results = []; foreach ($targets as $t) { $url = (string)($t['url'] ?? ''); if ($url === '' || !is_allowed_http_url($url, $allowedHosts)) { $results[] = ['url' => $url, 'state' => 'unknown', 'ms' => null, 'detail' => 'Nicht erlaubt']; continue; } $r = check_http_head($url); $results[] = [ 'url' => $url, 'state' => $r['ok'] ? 'up' : 'down', 'ms' => $r['ms'], 'detail' => $r['ok'] ? null : ($r['error'] ?? null), ]; } return $results; }