[ [ "title" => "Gitea", "description" => "Mein privater Gitea - Server", "url" => "/git", "logo" => "/icons/gitea.svg", "color" => "#609926", "external" => true ], [ "title" => "Nextcloud", "description" => "Meine persönliche Nextcloud", "url" => "/nextcloud", "logo" => "/icons/nextcloud.svg", "color" => "#0082c9", "external" => true ], [ "title" => "Server Dashboard", "description" => "Server-Verwaltung mit Cockpit", "url" => "https://cockpit.fabianschieder.com", "logo" => "/icons/ubuntu.svg", "color" => "#E95420", "external" => true ], ], "schule" => [ [ "title" => "Geizkragen", "description" => "Ein Online - Preisvergleichsportal", "url" => "https://geizkragen.store", "logo" => "/icons/geizkragen.png", "color" => "#0082c9", "external" => true ], ], "dienste" => [ [ "title" => "Home Assistant", "description" => "Mein privater HomeAssistant Server", "url" => "http://homeassistant.fabianschieder.com", "logo" => "/icons/homeassistant.svg", "color" => "#18BCF2", "external" => true ], [ "title" => "NAS", "description" => "Mein privater Netzwerkspeicher", "url" => "http://nas.fabianschieder.com", "logo" => "/icons/nas.svg", "color" => "#a855f7", "external" => true ], ], ]; // ── Serverstatus (dependency-free, mit Cache & SSRF-Guards) ─────────────── /** * Contract: * - Input: allowlist targets (host/url/port) * - Output: array of results with state + latency * - Safety: no user input, no redirects, blocks private/loopback IPs */ $serverStatusTargets = [ [ 'title' => 'Gitea', 'url' => 'https://fabianschieder.com/git', ], [ 'title' => 'Nextcloud', 'url' => 'https://fabianschieder.com/nextcloud', ], [ 'title' => 'Home Assistant', 'url' => 'http://homeassistant.fabianschieder.com', ], [ 'title' => 'NAS', 'url' => 'http://nas.fabianschieder.com', ], [ 'title' => 'Cockpit', 'url' => 'https://cockpit.fabianschieder.com', ], [ 'title' => 'Geizkragen', 'url' => 'https://geizkragen.store', ], ]; /** @return bool true if IP is public-ish */ function is_public_ip(string $ip): bool { // FILTER_FLAG_NO_PRIV_RANGE + NO_RES_RANGE covers most private/reserved ranges. if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return true; } // Fallback: treat unknown/invalid as not allowed. return false; } /** Resolve host to IPs (A/AAAA) */ function resolve_host_ips(string $host): array { $ips = []; // IPv4 $a = @dns_get_record($host, DNS_A); if (is_array($a)) { foreach ($a as $rec) { if (!empty($rec['ip'])) $ips[] = $rec['ip']; } } // IPv6 $aaaa = @dns_get_record($host, DNS_AAAA); if (is_array($aaaa)) { foreach ($aaaa as $rec) { if (!empty($rec['ipv6'])) $ips[] = $rec['ipv6']; } } // Deduplicate $ips = array_values(array_unique(array_filter($ips))); return $ips; } function is_allowed_http_url(string $url, array $allowedHosts): bool { $parts = parse_url($url); if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) return false; $scheme = strtolower((string)$parts['scheme']); if (!in_array($scheme, ['http', 'https'], true)) return false; $host = strtolower((string)$parts['host']); if (!in_array($host, $allowedHosts, true)) return false; // Resolve and ensure all resolved IPs are public. $ips = resolve_host_ips($host); if (empty($ips)) return false; foreach ($ips as $ip) { if (!is_public_ip($ip)) return false; } return true; } /** * HTTP HEAD check (no redirects). * @return array{ok:bool, code:int|null, ms:int|null, error:string|null} */ function check_http_head(string $url, float $timeoutSeconds = 1.6): array { $start = microtime(true); $ctx = stream_context_create([ 'http' => [ 'method' => 'HEAD', 'timeout' => $timeoutSeconds, 'ignore_errors' => true, 'follow_location' => 0, 'max_redirects' => 0, 'header' => "User-Agent: ServerStatus/1.0\r\n", ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $code = null; $error = null; set_error_handler(static function (int $severity, string $message) use (&$error): bool { $error = $message; return true; }); $fp = @fopen($url, 'rb', false, $ctx); restore_error_handler(); if ($fp !== false) { fclose($fp); } $ms = (int)round((microtime(true) - $start) * 1000); // Parse status code from $http_response_header if (isset($http_response_header) && is_array($http_response_header)) { foreach ($http_response_header as $h) { if (preg_match('#^HTTP/\S+\s+(\d{3})#i', (string)$h, $m)) { $code = (int)$m[1]; break; } } } $ok = ($code !== null) && ($code >= 200 && $code < 400); if ($code === null && $error === null) { $error = 'Unbekannter Fehler'; } return ['ok' => $ok, 'code' => $code, 'ms' => $ms, 'error' => $error]; } 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; } $cacheTtlSeconds = 30; $cacheKey = 'server_status_v1'; $cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json'; $serverStatus = null; $cacheOk = false; if (is_file($cacheFile)) { $raw = @file_get_contents($cacheFile); if ($raw !== false) { $decoded = json_decode($raw, true); if (is_array($decoded) && isset($decoded['ts'], $decoded['data']) && is_array($decoded['data'])) { $age = time() - (int)$decoded['ts']; if ($age >= 0 && $age <= $cacheTtlSeconds) { $serverStatus = $decoded['data']; $cacheOk = true; } } } } if (!is_array($serverStatus)) { $serverStatus = build_server_status($serverStatusTargets); @file_put_contents($cacheFile, json_encode(['ts' => time(), 'data' => $serverStatus], JSON_UNESCAPED_SLASHES)); } // Index: url => status $serverStatusByUrl = []; if (is_array($serverStatus)) { foreach ($serverStatus as $svc) { if (!empty($svc['url']) && is_string($svc['url'])) { $serverStatusByUrl[$svc['url']] = $svc; } } } ?>