From 33b1efb90342cdd26a06b7f18134eb07454d752b Mon Sep 17 00:00:00 2001 From: Fabian Schieder Date: Sun, 1 Mar 2026 14:14:17 +0100 Subject: [PATCH] Add custom 404 error page and enhance server status checks --- 503.php | 164 ++++++++++++++++++++++++++++++++ includes/lib/server_status.php | 63 +++++++++++- index.php | 2 +- scripts/dump_headers.php | 44 +++++++++ scripts/smoke_server_status.php | 10 +- 5 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 503.php create mode 100644 scripts/dump_headers.php diff --git a/503.php b/503.php new file mode 100644 index 0000000..21d3cf2 --- /dev/null +++ b/503.php @@ -0,0 +1,164 @@ + + + + + + + 404 – Seite nicht gefunden Β· Fabian Schieder + + + + + +
+
+ +
+
πŸ”
+ +
404
+ +
+ +

Seite nicht gefunden

+ +

+ Die Seite, die du suchst, existiert nicht (mehr)
+ oder wurde vielleicht verschoben. +

+ + +
+ + + + diff --git a/includes/lib/server_status.php b/includes/lib/server_status.php index fbab3d5..218c33c 100644 --- a/includes/lib/server_status.php +++ b/includes/lib/server_status.php @@ -99,13 +99,26 @@ function extract_http_status_code(array $headers): ?int /** * 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). + * Note: Redirects (3xx) are treated as not OK here because we intentionally don't follow redirects. + * Otherwise endpoints like /nextcloud (301 -> /nextcloud/ -> 503) would look "online". */ function is_reachable_http_code(?int $code): bool { if ($code === null) return false; - if ($code >= 200 && $code < 400) return true; + + // Explicitly DOWN: server-side failures like 503 Service Unavailable. + if ($code >= 500 && $code < 600) return false; + + // UP: normal success + if ($code >= 200 && $code < 300) return true; + + // UP: service is there but requires auth if ($code === 401 || $code === 403) return true; + + // UP: rate limited, but reachable + if ($code === 429) return true; + + // Redirects are handled separately (we do not follow them) return false; } @@ -181,12 +194,56 @@ function check_http_request(string $url, string $method, float $timeoutSeconds = } /** - * HTTP check (no redirects). Try HEAD first, then a minimal GET fallback (some servers lie on HEAD). + * HTTP check (no redirects). + * - Tries HEAD first. + * - If result is a redirect, probes the Location once (still without following further redirects). + * - If HEAD fails, tries a minimal GET with Range. * @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 we got a redirect, probe the Location target once, because we don't follow redirects automatically. + if ($head['code'] !== null && $head['code'] >= 300 && $head['code'] < 400) { + $loc = null; + if (isset($http_response_header) && is_array($http_response_header)) { + foreach ($http_response_header as $h) { + if (stripos((string)$h, 'Location:') === 0) { + $loc = trim(substr((string)$h, strlen('Location:'))); + break; + } + } + } + + if (is_string($loc) && $loc !== '') { + // Support relative redirects + if (str_starts_with($loc, '/')) { + $p = parse_url($url); + if (is_array($p) && !empty($p['scheme']) && !empty($p['host'])) { + $port = !empty($p['port']) ? ':' . (string)$p['port'] : ''; + $loc = strtolower((string)$p['scheme']) . '://' . (string)$p['host'] . $port . $loc; + } + } + + // Probe once; no further redirects. + $probe = check_http_request($loc, 'HEAD', $timeoutSeconds); + if ($probe['code'] === null) { + $probe = check_http_request($loc, 'GET', $timeoutSeconds, "Range: bytes=0-0\r\n"); + } + + // If the target is clearly reachable/unreachable, use that. + if ($probe['code'] !== null && ($probe['code'] < 300 || $probe['code'] >= 400)) { + return $probe; + } + + // Otherwise keep the redirect info, but don't mark it UP. + return ['ok' => false, 'code' => $head['code'], 'ms' => $head['ms'], 'error' => 'Redirect']; + } + + return ['ok' => false, 'code' => $head['code'], 'ms' => $head['ms'], 'error' => 'Redirect']; + } + if ($head['ok']) { return $head; } diff --git a/index.php b/index.php index 47c0efd..c6e8e4b 100644 --- a/index.php +++ b/index.php @@ -16,7 +16,7 @@ require_once __DIR__ . '/includes/lib/view_helpers.php'; // ── Serverstatus (mit Cache) ────────────────────────────────────────────── $cacheTtlSeconds = 30; -$cacheKey = 'server_status_v2'; +$cacheKey = 'server_status_v3'; $cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json'; $serverStatus = null; diff --git a/scripts/dump_headers.php b/scripts/dump_headers.php new file mode 100644 index 0000000..86534df --- /dev/null +++ b/scripts/dump_headers.php @@ -0,0 +1,44 @@ +\n"); + exit(2); +} + +$ctx = stream_context_create([ + 'http' => [ + 'follow_location' => 0, + 'max_redirects' => 0, + 'timeout' => 3.0, + 'ignore_errors' => true, + 'method' => 'GET', + 'header' => "User-Agent: HeaderDump/1.0\r\nRange: bytes=0-0\r\n", + ], + 'ssl' => [ + 'verify_peer' => true, + 'verify_peer_name' => true, + ], +]); + +$http_response_header = null; +set_error_handler(static function (int $severity, string $message): bool { + fwrite(STDERR, "PHP warning captured: $message\n"); + return true; +}); +$fp = @fopen($url, 'rb', false, $ctx); +restore_error_handler(); +if ($fp !== false) { + fclose($fp); +} + +if (!isset($http_response_header) || !is_array($http_response_header)) { + echo "(no headers)\n"; + exit(1); +} + +foreach ($http_response_header as $h) { + echo $h, "\n"; +} + diff --git a/scripts/smoke_server_status.php b/scripts/smoke_server_status.php index 700ca93..49eed8a 100644 --- a/scripts/smoke_server_status.php +++ b/scripts/smoke_server_status.php @@ -6,6 +6,7 @@ require __DIR__ . '/../includes/lib/server_status.php'; $targets = [ ['url' => 'https://example.com'], ['url' => 'https://example.com/does-not-exist-hopefully'], + ['url' => 'https://fabianschieder.com/nextcloud'], ]; foreach ($targets as $t) { @@ -15,5 +16,12 @@ foreach ($targets as $t) { echo " error=", $r['error']; } echo PHP_EOL; -} + // Zusatz: expliziter GET-Check (ohne Redirects), um 301/302 vs. 5xx besser zu sehen. + $g = check_http_request($t['url'], 'GET', 2.5, "Range: bytes=0-0\r\n"); + echo " GET => ", ($g['ok'] ? 'UP' : 'DOWN'), " code=", ($g['code'] ?? 'null'), " ms=", ($g['ms'] ?? 'null'); + if (!empty($g['error'])) { + echo " error=", $g['error']; + } + echo PHP_EOL; +}