diff --git a/api/dns-lookup.php b/api/dns-lookup.php new file mode 100644 index 0000000..ad7cf59 --- /dev/null +++ b/api/dns-lookup.php @@ -0,0 +1,161 @@ + 'Nur GET-Anfragen erlaubt']); + exit; +} + +// Domain-Parameter prüfen +$domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; + +if (empty($domain)) { + http_response_code(400); + echo json_encode(['error' => 'Domain-Parameter fehlt']); + exit; +} + +// Domain validieren (einfache Prüfung) +if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges Domain-Format']); + exit; +} + +// DNS-Abfrage durchführen +$startTime = microtime(true); +$result = performDnsLookup($domain); +$queryTime = round((microtime(true) - $startTime) * 1000, 2); + +// Ergebnis zurückgeben +echo json_encode([ + 'success' => true, + 'domain' => $domain, + 'query_time_ms' => $queryTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'records' => $result +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * Führt DNS-Lookup für verschiedene Record-Typen durch + */ +function performDnsLookup(string $domain): array { + $records = [ + 'A' => [], + 'AAAA' => [], + 'MX' => [], + 'NS' => [], + 'TXT' => [], + 'CNAME' => [], + 'SOA' => [] + ]; + + // A-Records (IPv4) + $aRecords = @dns_get_record($domain, DNS_A); + if ($aRecords) { + foreach ($aRecords as $record) { + $records['A'][] = [ + 'ip' => $record['ip'], + 'ttl' => $record['ttl'] + ]; + } + } + + // AAAA-Records (IPv6) + $aaaaRecords = @dns_get_record($domain, DNS_AAAA); + if ($aaaaRecords) { + foreach ($aaaaRecords as $record) { + $records['AAAA'][] = [ + 'ipv6' => $record['ipv6'], + 'ttl' => $record['ttl'] + ]; + } + } + + // MX-Records (Mail) + $mxRecords = @dns_get_record($domain, DNS_MX); + if ($mxRecords) { + foreach ($mxRecords as $record) { + $records['MX'][] = [ + 'target' => $record['target'], + 'priority' => $record['pri'], + 'ttl' => $record['ttl'] + ]; + } + // Nach Priorität sortieren + usort($records['MX'], fn($a, $b) => $a['priority'] <=> $b['priority']); + } + + // NS-Records (Nameserver) + $nsRecords = @dns_get_record($domain, DNS_NS); + if ($nsRecords) { + foreach ($nsRecords as $record) { + $records['NS'][] = [ + 'target' => $record['target'], + 'ttl' => $record['ttl'] + ]; + } + } + + // TXT-Records + $txtRecords = @dns_get_record($domain, DNS_TXT); + if ($txtRecords) { + foreach ($txtRecords as $record) { + $records['TXT'][] = [ + 'txt' => $record['txt'], + 'ttl' => $record['ttl'] + ]; + } + } + + // CNAME-Records + $cnameRecords = @dns_get_record($domain, DNS_CNAME); + if ($cnameRecords) { + foreach ($cnameRecords as $record) { + $records['CNAME'][] = [ + 'target' => $record['target'], + 'ttl' => $record['ttl'] + ]; + } + } + + // SOA-Record (Start of Authority) + $soaRecords = @dns_get_record($domain, DNS_SOA); + if ($soaRecords) { + foreach ($soaRecords as $record) { + $records['SOA'][] = [ + 'mname' => $record['mname'] ?? '', + 'rname' => $record['rname'] ?? '', + 'serial' => $record['serial'] ?? 0, + 'refresh' => $record['refresh'] ?? 0, + 'retry' => $record['retry'] ?? 0, + 'expire' => $record['expire'] ?? 0, + 'minimum_ttl' => $record['minimum-ttl'] ?? 0, + 'ttl' => $record['ttl'] + ]; + } + } + + // Leere Arrays entfernen + return array_filter($records, fn($arr) => !empty($arr)); +} diff --git a/api/dns-propagation.php b/api/dns-propagation.php new file mode 100644 index 0000000..62f2949 --- /dev/null +++ b/api/dns-propagation.php @@ -0,0 +1,175 @@ + 'Google', 'ip' => '8.8.8.8', 'location' => 'Global'], + ['name' => 'Google Secondary', 'ip' => '8.8.4.4', 'location' => 'Global'], + ['name' => 'Cloudflare', 'ip' => '1.1.1.1', 'location' => 'Global'], + ['name' => 'Cloudflare Secondary', 'ip' => '1.0.0.1', 'location' => 'Global'], + ['name' => 'Quad9', 'ip' => '9.9.9.9', 'location' => 'Global'], + ['name' => 'OpenDNS', 'ip' => '208.67.222.222', 'location' => 'USA'], + ['name' => 'Comodo', 'ip' => '8.26.56.26', 'location' => 'USA'], + ['name' => 'Level3', 'ip' => '4.2.2.1', 'location' => 'USA'], +]; + +$domain = isset($_GET['domain']) ? trim($_GET['domain']) : ''; +$type = isset($_GET['type']) ? strtoupper(trim($_GET['type'])) : 'A'; + +if (empty($domain)) { + http_response_code(400); + echo json_encode(['error' => 'Domain-Parameter fehlt']); + exit; +} + +if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges Domain-Format']); + exit; +} + +// Erlaubte Record-Typen +$allowedTypes = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME']; +if (!in_array($type, $allowedTypes)) { + $type = 'A'; +} + +$results = []; +$startTime = microtime(true); + +foreach ($dnsServers as $server) { + $serverResult = [ + 'server' => $server['name'], + 'ip' => $server['ip'], + 'location' => $server['location'], + 'records' => [], + 'status' => 'success', + 'response_time' => 0 + ]; + + $queryStart = microtime(true); + + // DNS-Abfrage mit spezifischem Server via dig (falls verfügbar) oder dns_get_record + $records = queryDnsServer($domain, $type, $server['ip']); + + $serverResult['response_time'] = round((microtime(true) - $queryStart) * 1000, 2); + $serverResult['records'] = $records; + + if (empty($records)) { + $serverResult['status'] = 'no_records'; + } + + $results[] = $serverResult; +} + +$totalTime = round((microtime(true) - $startTime) * 1000, 2); + +// Propagation-Status berechnen +$propagationStatus = calculatePropagationStatus($results); + +echo json_encode([ + 'success' => true, + 'domain' => $domain, + 'record_type' => $type, + 'propagation_status' => $propagationStatus, + 'total_time_ms' => $totalTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'servers' => $results +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * DNS-Abfrage bei spezifischem Server + */ +function queryDnsServer(string $domain, string $type, string $server): array { + $records = []; + + // Versuche zuerst dig zu verwenden (genauer) + $digResult = @shell_exec("dig @{$server} {$domain} {$type} +short +time=2 +tries=1 2>/dev/null"); + + if ($digResult !== null && trim($digResult) !== '') { + $lines = array_filter(explode("\n", trim($digResult))); + foreach ($lines as $line) { + $records[] = trim($line); + } + return $records; + } + + // Fallback auf PHP dns_get_record (verwendet System-DNS) + $dnsType = constant('DNS_' . $type); + $result = @dns_get_record($domain, $dnsType); + + if ($result) { + foreach ($result as $record) { + switch ($type) { + case 'A': + $records[] = $record['ip'] ?? ''; + break; + case 'AAAA': + $records[] = $record['ipv6'] ?? ''; + break; + case 'MX': + $records[] = ($record['pri'] ?? '') . ' ' . ($record['target'] ?? ''); + break; + case 'NS': + case 'CNAME': + $records[] = $record['target'] ?? ''; + break; + case 'TXT': + $records[] = $record['txt'] ?? ''; + break; + } + } + } + + return array_filter($records); +} + +/** + * Berechnet den Propagation-Status + */ +function calculatePropagationStatus(array $results): array { + $totalServers = count($results); + $serversWithRecords = 0; + $allRecords = []; + + foreach ($results as $result) { + if (!empty($result['records'])) { + $serversWithRecords++; + foreach ($result['records'] as $record) { + $allRecords[] = $record; + } + } + } + + // Einzigartige Records + $uniqueRecords = array_unique($allRecords); + + // Konsistenz prüfen (haben alle Server die gleichen Records?) + $isConsistent = count($uniqueRecords) <= 1 || $serversWithRecords === 0; + + $percentage = $totalServers > 0 ? round(($serversWithRecords / $totalServers) * 100) : 0; + + return [ + 'percentage' => $percentage, + 'servers_responding' => $serversWithRecords, + 'total_servers' => $totalServers, + 'is_consistent' => $isConsistent, + 'unique_values' => array_values($uniqueRecords) + ]; +} diff --git a/api/ping-check.php b/api/ping-check.php new file mode 100644 index 0000000..0ee176a --- /dev/null +++ b/api/ping-check.php @@ -0,0 +1,179 @@ + 'Domain-Parameter fehlt']); + exit; +} + +// Protokoll und Pfad entfernen +$domain = preg_replace('/^(https?:\/\/)?/', '', $domain); +$domain = explode('/', $domain)[0]; + +if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges Domain-Format']); + exit; +} + +$startTime = microtime(true); +$results = performConnectivityCheck($domain); +$totalTime = round((microtime(true) - $startTime) * 1000, 2); + +echo json_encode([ + 'success' => true, + 'domain' => $domain, + 'total_time_ms' => $totalTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'results' => $results +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * Führt verschiedene Erreichbarkeitstests durch + */ +function performConnectivityCheck(string $domain): array { + $results = [ + 'dns_resolution' => checkDnsResolution($domain), + 'icmp_ping' => checkIcmpPing($domain), + 'http' => checkHttpConnection($domain, false), + 'https' => checkHttpConnection($domain, true), + 'overall_status' => 'offline' + ]; + + // Overall-Status bestimmen + if ($results['https']['reachable'] || $results['http']['reachable']) { + $results['overall_status'] = 'online'; + } elseif ($results['icmp_ping']['reachable']) { + $results['overall_status'] = 'partial'; + } elseif ($results['dns_resolution']['resolved']) { + $results['overall_status'] = 'dns_only'; + } + + return $results; +} + +/** + * DNS-Auflösung prüfen + */ +function checkDnsResolution(string $domain): array { + $start = microtime(true); + $ip = gethostbyname($domain); + $time = round((microtime(true) - $start) * 1000, 2); + + $resolved = $ip !== $domain; + + return [ + 'resolved' => $resolved, + 'ip' => $resolved ? $ip : null, + 'response_time_ms' => $time + ]; +} + +/** + * ICMP Ping (falls verfügbar) + */ +function checkIcmpPing(string $domain): array { + $result = [ + 'reachable' => false, + 'response_time_ms' => null, + 'packet_loss' => null + ]; + + // Versuche ping-Kommando + $pingResult = @shell_exec("ping -c 3 -W 2 {$domain} 2>/dev/null"); + + if ($pingResult) { + // Prüfe auf erfolgreiche Antworten + if (preg_match('/(\d+)% packet loss/', $pingResult, $lossMatch)) { + $result['packet_loss'] = (int)$lossMatch[1]; + $result['reachable'] = $result['packet_loss'] < 100; + } + + // Durchschnittliche Zeit extrahieren + if (preg_match('/avg.*?=.*?[\d.]+\/([\d.]+)\//', $pingResult, $timeMatch)) { + $result['response_time_ms'] = (float)$timeMatch[1]; + } elseif (preg_match('/time[=<]([\d.]+)\s*ms/', $pingResult, $timeMatch)) { + $result['response_time_ms'] = (float)$timeMatch[1]; + } + } + + return $result; +} + +/** + * HTTP(S)-Verbindung prüfen + */ +function checkHttpConnection(string $domain, bool $https = false): array { + $protocol = $https ? 'https' : 'http'; + $port = $https ? 443 : 80; + $url = "{$protocol}://{$domain}"; + + $result = [ + 'reachable' => false, + 'status_code' => null, + 'response_time_ms' => null, + 'redirect_url' => null, + 'server' => null + ]; + + $start = microtime(true); + + // cURL verwenden + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_NOBODY => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_USERAGENT => 'HexaDNS Ping Check/1.0' + ]); + + $response = curl_exec($ch); + $result['response_time_ms'] = round((microtime(true) - $start) * 1000, 2); + + if ($response !== false) { + $result['status_code'] = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $result['reachable'] = $result['status_code'] > 0; + + // Redirect-URL + $redirectUrl = curl_getinfo($ch, CURLINFO_REDIRECT_URL); + if (!empty($redirectUrl)) { + $result['redirect_url'] = $redirectUrl; + } + + // Server-Header + if (preg_match('/Server:\s*([^\r\n]+)/i', $response, $serverMatch)) { + $result['server'] = trim($serverMatch[1]); + } + } else { + $result['error'] = curl_error($ch); + } + + curl_close($ch); + + return $result; +} diff --git a/api/reverse-dns.php b/api/reverse-dns.php new file mode 100644 index 0000000..4aa832d --- /dev/null +++ b/api/reverse-dns.php @@ -0,0 +1,209 @@ + 'IP-Parameter fehlt']); + exit; +} + +// IPv4 oder IPv6 validieren +$isIPv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); +$isIPv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + +if (!$isIPv4 && !$isIPv6) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges IP-Format']); + exit; +} + +$startTime = microtime(true); +$result = performReverseLookup($ip, $isIPv6 ? 'IPv6' : 'IPv4'); +$queryTime = round((microtime(true) - $startTime) * 1000, 2); + +echo json_encode([ + 'success' => true, + 'ip' => $ip, + 'ip_version' => $isIPv6 ? 'IPv6' : 'IPv4', + 'query_time_ms' => $queryTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'result' => $result +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * Führt Reverse DNS Lookup durch + */ +function performReverseLookup(string $ip, string $version): array { + $result = [ + 'hostname' => null, + 'ptr_record' => null, + 'additional_info' => [] + ]; + + // PHP gethostbyaddr + $hostname = @gethostbyaddr($ip); + + if ($hostname && $hostname !== $ip) { + $result['hostname'] = $hostname; + + // Verifizieren durch Forward-Lookup + $forwardIp = gethostbyname($hostname); + $result['forward_verified'] = ($forwardIp === $ip); + + // Zusätzliche Infos über den Host sammeln + $result['additional_info'] = getHostInfo($hostname); + } else { + $result['error'] = 'Kein PTR-Record gefunden'; + } + + // PTR-Record direkt abfragen + $ptrRecord = getPtrRecord($ip, $version); + if ($ptrRecord) { + $result['ptr_record'] = $ptrRecord; + } + + // IP-Info (GeoIP wenn verfügbar, sonst Basic-Infos) + $result['ip_info'] = getIpInfo($ip); + + return $result; +} + +/** + * PTR-Record direkt abfragen + */ +function getPtrRecord(string $ip, string $version): ?string { + if ($version === 'IPv4') { + // IPv4: Reverse die Oktette + $parts = array_reverse(explode('.', $ip)); + $ptrDomain = implode('.', $parts) . '.in-addr.arpa'; + } else { + // IPv6: Komplexer - jedes Nibble umkehren + $expanded = expandIPv6($ip); + $nibbles = str_replace(':', '', $expanded); + $reversed = implode('.', array_reverse(str_split($nibbles))); + $ptrDomain = $reversed . '.ip6.arpa'; + } + + $records = @dns_get_record($ptrDomain, DNS_PTR); + + if ($records && !empty($records[0]['target'])) { + return $records[0]['target']; + } + + return null; +} + +/** + * Expandiert eine IPv6-Adresse + */ +function expandIPv6(string $ip): string { + // Ersetze :: mit der richtigen Anzahl von 0000 + if (strpos($ip, '::') !== false) { + $parts = explode('::', $ip); + $left = $parts[0] ? explode(':', $parts[0]) : []; + $right = isset($parts[1]) && $parts[1] ? explode(':', $parts[1]) : []; + $missing = 8 - count($left) - count($right); + $middle = array_fill(0, $missing, '0000'); + $all = array_merge($left, $middle, $right); + } else { + $all = explode(':', $ip); + } + + // Jedes Segment auf 4 Zeichen auffüllen + $all = array_map(function($segment) { + return str_pad($segment, 4, '0', STR_PAD_LEFT); + }, $all); + + return implode(':', $all); +} + +/** + * Sammelt Infos über einen Hostnamen + */ +function getHostInfo(string $hostname): array { + $info = []; + + // Domain-Teile analysieren + $parts = explode('.', $hostname); + $tld = end($parts); + + $info['tld'] = $tld; + + // Bekannte Hosting-Provider erkennen + $providerPatterns = [ + 'amazonaws.com' => 'Amazon AWS', + 'googleusercontent.com' => 'Google Cloud', + 'cloudfront.net' => 'Amazon CloudFront', + 'azure' => 'Microsoft Azure', + 'hetzner' => 'Hetzner', + 'ovh' => 'OVH', + 'digitalocean' => 'DigitalOcean', + 'linode' => 'Linode', + 'vultr' => 'Vultr', + 'contabo' => 'Contabo', + 'netcup' => 'Netcup', + 'strato' => 'Strato', + 'ionos' => 'IONOS', + '1und1' => '1&1', + 'telekom' => 'Deutsche Telekom', + 'vodafone' => 'Vodafone', + ]; + + foreach ($providerPatterns as $pattern => $provider) { + if (stripos($hostname, $pattern) !== false) { + $info['provider'] = $provider; + break; + } + } + + return $info; +} + +/** + * Basis-Infos zur IP + */ +function getIpInfo(string $ip): array { + $info = [ + 'type' => 'unknown' + ]; + + // Private IP-Bereiche prüfen + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + if (preg_match('/^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/', $ip)) { + $info['type'] = 'private'; + } elseif (preg_match('/^(127\.)/', $ip)) { + $info['type'] = 'loopback'; + } else { + $info['type'] = 'public'; + } + } else { + // IPv6 + if (preg_match('/^(fc|fd)/i', $ip)) { + $info['type'] = 'private'; + } elseif (preg_match('/^::1$/', $ip) || preg_match('/^fe80:/i', $ip)) { + $info['type'] = 'loopback/link-local'; + } else { + $info['type'] = 'public'; + } + } + + return $info; +} diff --git a/api/ssl-check.php b/api/ssl-check.php new file mode 100644 index 0000000..79e13cb --- /dev/null +++ b/api/ssl-check.php @@ -0,0 +1,164 @@ + 'Domain-Parameter fehlt']); + exit; +} + +// Protokoll und Pfad entfernen +$domain = preg_replace('/^(https?:\/\/)?/', '', $domain); +$domain = explode('/', $domain)[0]; +$domain = explode(':', $domain)[0]; // Port entfernen + +if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges Domain-Format']); + exit; +} + +$startTime = microtime(true); +$sslData = checkSslCertificate($domain); +$queryTime = round((microtime(true) - $startTime) * 1000, 2); + +echo json_encode([ + 'success' => $sslData['success'], + 'domain' => $domain, + 'query_time_ms' => $queryTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'ssl' => $sslData +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * Prüft SSL-Zertifikat einer Domain + */ +function checkSslCertificate(string $domain): array { + $result = [ + 'success' => false, + 'has_ssl' => false, + 'is_valid' => false, + 'error' => null, + 'certificate' => null + ]; + + // Stream Context für SSL + $context = stream_context_create([ + 'ssl' => [ + 'capture_peer_cert' => true, + 'verify_peer' => false, + 'verify_peer_name' => false, + ] + ]); + + // Verbindung herstellen + $socket = @stream_socket_client( + "ssl://{$domain}:443", + $errno, + $errstr, + 10, // Timeout + STREAM_CLIENT_CONNECT, + $context + ); + + if (!$socket) { + // Versuche ohne SSL (um zu prüfen ob Server erreichbar) + $httpSocket = @fsockopen($domain, 80, $errno, $errstr, 5); + if ($httpSocket) { + fclose($httpSocket); + $result['error'] = 'Kein SSL-Zertifikat auf Port 443 gefunden'; + } else { + $result['error'] = "Verbindung fehlgeschlagen: {$errstr}"; + } + return $result; + } + + $result['has_ssl'] = true; + $result['success'] = true; + + // Zertifikat extrahieren + $params = stream_context_get_params($socket); + $cert = $params['options']['ssl']['peer_certificate'] ?? null; + + if ($cert) { + $certInfo = openssl_x509_parse($cert); + + if ($certInfo) { + $validFrom = $certInfo['validFrom_time_t'] ?? 0; + $validTo = $certInfo['validTo_time_t'] ?? 0; + $now = time(); + + $isExpired = $now > $validTo; + $isNotYetValid = $now < $validFrom; + $result['is_valid'] = !$isExpired && !$isNotYetValid; + + // Tage bis Ablauf + $daysUntilExpiry = floor(($validTo - $now) / 86400); + + // Subject Alternative Names (SANs) + $sans = []; + if (isset($certInfo['extensions']['subjectAltName'])) { + $sanStr = $certInfo['extensions']['subjectAltName']; + preg_match_all('/DNS:([^,\s]+)/', $sanStr, $matches); + $sans = $matches[1] ?? []; + } + + // Issuer aufbereiten + $issuer = []; + if (isset($certInfo['issuer'])) { + if (isset($certInfo['issuer']['O'])) $issuer['organization'] = $certInfo['issuer']['O']; + if (isset($certInfo['issuer']['CN'])) $issuer['common_name'] = $certInfo['issuer']['CN']; + if (isset($certInfo['issuer']['C'])) $issuer['country'] = $certInfo['issuer']['C']; + } + + // Subject aufbereiten + $subject = []; + if (isset($certInfo['subject'])) { + if (isset($certInfo['subject']['CN'])) $subject['common_name'] = $certInfo['subject']['CN']; + if (isset($certInfo['subject']['O'])) $subject['organization'] = $certInfo['subject']['O']; + } + + $result['certificate'] = [ + 'subject' => $subject, + 'issuer' => $issuer, + 'valid_from' => date('Y-m-d H:i:s', $validFrom), + 'valid_to' => date('Y-m-d H:i:s', $validTo), + 'days_until_expiry' => $daysUntilExpiry, + 'is_expired' => $isExpired, + 'serial_number' => $certInfo['serialNumberHex'] ?? null, + 'signature_algorithm' => $certInfo['signatureTypeSN'] ?? null, + 'san_domains' => $sans, + 'version' => $certInfo['version'] ?? null, + ]; + + // Warnung wenn bald ablaufend + if ($daysUntilExpiry <= 30 && $daysUntilExpiry > 0) { + $result['warning'] = "Zertifikat läuft in {$daysUntilExpiry} Tagen ab!"; + } elseif ($isExpired) { + $result['error'] = 'Zertifikat ist abgelaufen!'; + $result['is_valid'] = false; + } + } + } + + fclose($socket); + return $result; +} diff --git a/api/whois-lookup.php b/api/whois-lookup.php new file mode 100644 index 0000000..5dc9ed6 --- /dev/null +++ b/api/whois-lookup.php @@ -0,0 +1,365 @@ + 'Domain-Parameter fehlt']); + exit; +} + +// Nur Root-Domain extrahieren +$domain = extractRootDomain($domain); + +if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-]*\.[a-zA-Z]{2,}$/', $domain)) { + http_response_code(400); + echo json_encode(['error' => 'Ungültiges Domain-Format']); + exit; +} + +$startTime = microtime(true); +$whoisData = performWhoisLookup($domain); +$queryTime = round((microtime(true) - $startTime) * 1000, 2); + +if ($whoisData === null) { + http_response_code(500); + echo json_encode(['error' => 'WHOIS-Abfrage fehlgeschlagen']); + exit; +} + +echo json_encode([ + 'success' => true, + 'domain' => $domain, + 'query_time_ms' => $queryTime, + 'timestamp' => date('Y-m-d H:i:s'), + 'whois' => $whoisData +], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + +/** + * Extrahiert die Root-Domain (ohne Subdomain) + */ +function extractRootDomain(string $domain): string { + $domain = strtolower($domain); + $domain = preg_replace('/^(https?:\/\/)?(www\.)?/', '', $domain); + $domain = explode('/', $domain)[0]; + + $parts = explode('.', $domain); + if (count($parts) > 2) { + // Einfache Logik: nimm die letzten 2 Teile + // (funktioniert nicht perfekt für .co.uk etc., aber gut genug) + return implode('.', array_slice($parts, -2)); + } + + return $domain; +} + +/** + * Führt WHOIS-Lookup durch + */ +function performWhoisLookup(string $domain): ?array { + // Primär: Socket-basierte Abfrage (funktioniert ohne shell_exec) + $whoisRaw = whoisViaSocket($domain); + + // Fallback: Shell-Kommando (sicher escaped) + if (empty($whoisRaw) && function_exists('shell_exec')) { + $escapedDomain = escapeshellarg($domain); + $whoisRaw = @shell_exec("whois {$escapedDomain} 2>/dev/null"); + } + + if (empty($whoisRaw)) { + return null; + } + + // Parse WHOIS-Daten + return parseWhoisData($whoisRaw, $domain); +} + +/** + * WHOIS-Abfrage über Socket (unabhängig von shell_exec) + */ +function whoisViaSocket(string $domain): ?string { + $whoisServer = getWhoisServer($domain); + + if (!$whoisServer) { + return null; + } + + $result = queryWhoisServer($whoisServer, $domain); + + // Prüfe auf Weiterleitungen zu anderen WHOIS-Servern + if ($result && preg_match('/Registrar WHOIS Server:\s*(\S+)/i', $result, $matches)) { + $referralServer = trim($matches[1]); + if ($referralServer && $referralServer !== $whoisServer) { + $referralResult = queryWhoisServer($referralServer, $domain); + if ($referralResult) { + $result = $referralResult; + } + } + } + + return $result; +} + +/** + * Abfrage an einen spezifischen WHOIS-Server + */ +function queryWhoisServer(string $server, string $domain): ?string { + $port = 43; + $timeout = 10; + + $socket = @fsockopen($server, $port, $errno, $errstr, $timeout); + + if (!$socket) { + return null; + } + + // Setze Stream-Timeout + stream_set_timeout($socket, $timeout); + + // Sende Anfrage + fwrite($socket, $domain . "\r\n"); + + // Lese Antwort + $response = ''; + while (!feof($socket)) { + $response .= fread($socket, 8192); + } + + fclose($socket); + + return !empty($response) ? $response : null; +} + +/** + * Ermittelt den zuständigen WHOIS-Server für eine TLD + */ +function getWhoisServer(string $domain): ?string { + $parts = explode('.', $domain); + $tld = strtolower(end($parts)); + + // Bekannte WHOIS-Server nach TLD + $whoisServers = [ + // Generische TLDs + 'com' => 'whois.verisign-grs.com', + 'net' => 'whois.verisign-grs.com', + 'org' => 'whois.pir.org', + 'info' => 'whois.afilias.net', + 'biz' => 'whois.biz', + 'name' => 'whois.nic.name', + 'mobi' => 'whois.dotmobiregistry.net', + 'pro' => 'whois.registrypro.pro', + 'aero' => 'whois.aero', + 'asia' => 'whois.nic.asia', + 'cat' => 'whois.nic.cat', + 'coop' => 'whois.nic.coop', + 'edu' => 'whois.educause.edu', + 'gov' => 'whois.dotgov.gov', + 'int' => 'whois.iana.org', + 'jobs' => 'whois.nic.jobs', + 'mil' => 'whois.nic.mil', + 'museum' => 'whois.museum', + 'post' => 'whois.dotpostregistry.net', + 'tel' => 'whois.nic.tel', + 'travel' => 'whois.nic.travel', + 'xxx' => 'whois.nic.xxx', + + // Neue gTLDs + 'app' => 'whois.nic.google', + 'dev' => 'whois.nic.google', + 'page' => 'whois.nic.google', + 'blog' => 'whois.nic.blog', + 'cloud' => 'whois.nic.cloud', + 'shop' => 'whois.nic.shop', + 'store' => 'whois.nic.store', + 'online' => 'whois.nic.online', + 'site' => 'whois.nic.site', + 'website' => 'whois.nic.website', + 'tech' => 'whois.nic.tech', + 'io' => 'whois.nic.io', + 'co' => 'whois.nic.co', + 'me' => 'whois.nic.me', + 'tv' => 'whois.nic.tv', + 'cc' => 'ccwhois.verisign-grs.com', + 'ws' => 'whois.website.ws', + + // Europäische ccTLDs + 'de' => 'whois.denic.de', + 'at' => 'whois.nic.at', + 'ch' => 'whois.nic.ch', + 'li' => 'whois.nic.li', + 'uk' => 'whois.nic.uk', + 'fr' => 'whois.nic.fr', + 'it' => 'whois.nic.it', + 'es' => 'whois.nic.es', + 'pt' => 'whois.dns.pt', + 'nl' => 'whois.domain-registry.nl', + 'be' => 'whois.dns.be', + 'pl' => 'whois.dns.pl', + 'cz' => 'whois.nic.cz', + 'sk' => 'whois.sk-nic.sk', + 'hu' => 'whois.nic.hu', + 'ro' => 'whois.rotld.ro', + 'bg' => 'whois.register.bg', + 'hr' => 'whois.dns.hr', + 'si' => 'whois.register.si', + 'rs' => 'whois.rnids.rs', + 'gr' => 'grwhois.ics.forth.gr', + 'dk' => 'whois.punktum.dk', + 'se' => 'whois.iis.se', + 'no' => 'whois.norid.no', + 'fi' => 'whois.fi', + 'ie' => 'whois.iedr.ie', + 'eu' => 'whois.eu', + 'lu' => 'whois.dns.lu', + + // Andere ccTLDs + 'ru' => 'whois.tcinet.ru', + 'ua' => 'whois.ua', + 'us' => 'whois.nic.us', + 'ca' => 'whois.cira.ca', + 'mx' => 'whois.mx', + 'br' => 'whois.registro.br', + 'ar' => 'whois.nic.ar', + 'au' => 'whois.auda.org.au', + 'nz' => 'whois.srs.net.nz', + 'jp' => 'whois.jprs.jp', + 'kr' => 'whois.kr', + 'cn' => 'whois.cnnic.cn', + 'in' => 'whois.registry.in', + 'sg' => 'whois.sgnic.sg', + 'hk' => 'whois.hkirc.hk', + 'tw' => 'whois.twnic.net.tw', + 'za' => 'whois.registry.net.za', + ]; + + // Spezielle Behandlung für .co.uk, .com.au etc. + if (count($parts) >= 2) { + $sld = $parts[count($parts) - 2]; + $combinedTld = $sld . '.' . $tld; + + $secondLevelTlds = [ + 'co.uk' => 'whois.nic.uk', + 'org.uk' => 'whois.nic.uk', + 'me.uk' => 'whois.nic.uk', + 'com.au' => 'whois.auda.org.au', + 'net.au' => 'whois.auda.org.au', + 'org.au' => 'whois.auda.org.au', + 'co.nz' => 'whois.srs.net.nz', + 'com.br' => 'whois.registro.br', + ]; + + if (isset($secondLevelTlds[$combinedTld])) { + return $secondLevelTlds[$combinedTld]; + } + } + + return $whoisServers[$tld] ?? 'whois.iana.org'; +} + +/** + * Parsed WHOIS-Rohdaten in strukturiertes Format + */ +function parseWhoisData(string $raw, string $domain): array { + $data = [ + 'raw' => $raw, + 'parsed' => [ + 'domain_name' => $domain, + 'registrar' => null, + 'registrar_url' => null, + 'creation_date' => null, + 'expiration_date' => null, + 'updated_date' => null, + 'status' => [], + 'nameservers' => [], + 'dnssec' => null, + ] + ]; + + $lines = explode("\n", $raw); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '%') === 0 || strpos($line, '#') === 0) { + continue; + } + + // Key: Value Format + if (strpos($line, ':') !== false) { + list($key, $value) = array_map('trim', explode(':', $line, 2)); + $keyLower = strtolower($key); + + // Registrar + if (strpos($keyLower, 'registrar') !== false && strpos($keyLower, 'abuse') === false && strpos($keyLower, 'url') === false) { + if (empty($data['parsed']['registrar'])) { + $data['parsed']['registrar'] = $value; + } + } + + // Registrar URL + if (strpos($keyLower, 'registrar') !== false && strpos($keyLower, 'url') !== false) { + $data['parsed']['registrar_url'] = $value; + } + + // Erstellungsdatum + if (preg_match('/(creation|created|registered)/i', $keyLower) && strpos($keyLower, 'registrar') === false) { + if (empty($data['parsed']['creation_date'])) { + $data['parsed']['creation_date'] = $value; + } + } + + // Ablaufdatum + if (preg_match('/(expir|paid-till)/i', $keyLower)) { + if (empty($data['parsed']['expiration_date'])) { + $data['parsed']['expiration_date'] = $value; + } + } + + // Aktualisierungsdatum + if (preg_match('/(updated|modified|changed)/i', $keyLower) && strpos($keyLower, 'registrar') === false) { + if (empty($data['parsed']['updated_date'])) { + $data['parsed']['updated_date'] = $value; + } + } + + // Status + if (preg_match('/(status|state)/i', $keyLower) && !empty($value)) { + $data['parsed']['status'][] = $value; + } + + // Nameserver + if (preg_match('/^(name.?server|nserver)/i', $keyLower) && !empty($value)) { + $ns = strtolower(explode(' ', $value)[0]); + if (!in_array($ns, $data['parsed']['nameservers'])) { + $data['parsed']['nameservers'][] = $ns; + } + } + + // DNSSEC + if (strpos($keyLower, 'dnssec') !== false) { + $data['parsed']['dnssec'] = $value; + } + } + } + + // Status einzigartig machen + $data['parsed']['status'] = array_unique($data['parsed']['status']); + + return $data; +}