176 lines
4.5 KiB
PHP
176 lines
4.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
/** @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;
|
|
}
|
|
|