From e40926c08d245fc1df41873b3d78465bf83f0ff5 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 16 Jul 2026 19:30:38 -0400 Subject: [PATCH] Restore native DNS/RDAP domain lookups (lost in functions.php split) getDnsRecords and getDomainExpirationDate had regressed to shell_exec (dig/whois) when domain code was moved into functions/domain.php; the RDAP helper stack was dropped entirely. Restores the dns_get_record + RDAP + port-43-socket-whois implementation from d1e1609b under the current function names. getSslCertificate was unaffected. --- functions/domain.php | 532 ++++++++++++++++++++++++++++++------------- 1 file changed, 368 insertions(+), 164 deletions(-) diff --git a/functions/domain.php b/functions/domain.php index 026a334e..b305b50d 100644 --- a/functions/domain.php +++ b/functions/domain.php @@ -1,79 +1,9 @@ true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 10, + CURLOPT_USERAGENT => 'ITFlow-Domain-Check', + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_FOLLOWLOCATION => true, // RDAP bootstrap/redirectors use 30x + CURLOPT_MAXREDIRS => 5, + CURLOPT_HTTPHEADER => array('Accept: application/rdap+json'), + )); + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return ($response === false) ? '' : $response; +} + +function getDomainRdap($domain) { + static $cache = array(); + + if (array_key_exists($domain, $cache)) { + return $cache[$domain]; } + $cache[$domain] = null; + + $tld = substr(strrchr($domain, '.'), 1); + if (empty($tld)) { + return null; + } + + // Primary: IANA bootstrap -> registry RDAP server directly + $base = getRdapBaseUrl($tld); + if (!empty($base)) { + $raw = rdapHttpGet($base . 'domain/' . rawurlencode($domain), $code); + if ($code === 200 && !empty($raw)) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $cache[$domain] = $decoded; + return $decoded; + } + } + if ($code === 404) { + return null; // Domain genuinely not found - don't bother the fallback + } + } + + // Fallback: rdap.org redirector (covers gaps and bootstrap fetch failures) + $raw = rdapHttpGet('https://rdap.org/domain/' . rawurlencode($domain), $code); + if ($code === 200 && !empty($raw)) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $cache[$domain] = $decoded; + } + } + + return $cache[$domain]; +} - $expireDate = ''; +function getRdapEventDate($rdap, $action) { + if (empty($rdap['events']) || !is_array($rdap['events'])) { + return ''; + } + foreach ($rdap['events'] as $event) { + if (isset($event['eventAction'], $event['eventDate']) && $event['eventAction'] === $action) { + return $event['eventDate']; + } + } + return ''; +} - // Regular expressions to match different date formats - $patterns = [ - '/Expiration Date: (.+)/', - '/Registry Expiry Date: (.+)/', - '/expires: (.+)/', - '/Expiry Date: (.+)/', - '/renewal date: (.+)/', - '/Expires On: (.+)/', - '/paid-till: (.+)/', - '/Expiration Time: (.+)/', - '/\[Expires on\]\s+(.+)/', - '/expire: (.+)/', - '/validity: (.+)/', - '/Expires on.*: (.+)/i', - '/Expiry on.*: (.+)/i', - '/renewal: (.+)/i', - '/Expir\w+ Date: (.+)/i', - '/Valid Until: (.+)/i', - '/Valid until: (.+)/i', - '/expire-date: (.+)/i', - '/Expiration Date: (.+)/i', - '/Registry Expiry Date: (.+)/i', - '/Expire Date: (.+)/i', - '/expiry: (.+)/i', - '/expires: (.+)/i', - '/Registry Expiry Date: (.+)/i', - '/Expiration Time: (.+)/i', - '/validity: (.+)/i', - '/expires: (.+)/i', - '/paid-till: (.+)/i', - '/Expire Date: (.+)/i', - '/Expiration Date: (.+)/i', - '/expire: (.+)/i', - '/expiry: (.+)/i', - '/renewal date: (.+)/i', - '/Expiration Date: (.+)/i', - '/Expiration Time: (.+)/i', - '/Expires: (.+)/i', - ]; +function getRdapRegistrar($rdap) { + if (empty($rdap['entities']) || !is_array($rdap['entities'])) { + return ''; + } + foreach ($rdap['entities'] as $entity) { + if (empty($entity['roles']) || !in_array('registrar', $entity['roles'])) { + continue; + } + if (!empty($entity['vcardArray'][1]) && is_array($entity['vcardArray'][1])) { + foreach ($entity['vcardArray'][1] as $field) { + if (isset($field[0], $field[3]) && $field[0] === 'fn' && is_string($field[3])) { + return $field[3]; + } + } + } + } + return ''; +} - // Known date formats - $knownFormats = [ - "d-M-Y", - "d-F-Y", - "d-m-Y", - "Y-m-d", - "d.m.Y", - "Y.m.d", - "Y/m/d", - "Y/m/d H:i:s", - "Ymd", - "Ymd H:i:s", - "d/m/Y", - "Y. m. d.", - "Y.m.d H:i:s", - "d-M-Y H:i:s", - "D M d H:i:s T Y", - "D M d Y", - "Y-m-d\TH:i:s", - "Y-m-d\TH:i:s\Z", - "Y-m-d H:i:s\Z", - "Y-m-d H:i:s", - "d M Y H:i:s", - "d/m/Y H:i:s", - "d/m/Y H:i:s T", - "B d Y", - "d.m.Y H:i:s", - "before M-Y", - "before Y-m-d", - "before Ymd", - "Y-m-d H:i:s (\T\Z\Z)", - "Y-M-d.", - ]; +function getRdapSummary($rdap) { + $lines = array(); + + $registrar = getRdapRegistrar($rdap); + if (!empty($registrar)) { + $lines[] = "Registrar: $registrar"; + } + + $registered = getRdapEventDate($rdap, 'registration'); + if (!empty($registered)) { + $lines[] = 'Registered: ' . substr($registered, 0, 10); + } + + $expires = getRdapEventDate($rdap, 'expiration'); + if (!empty($expires)) { + $lines[] = 'Expires: ' . substr($expires, 0, 10); + } + + if (!empty($rdap['status']) && is_array($rdap['status'])) { + $lines[] = 'Status: ' . implode(', ', array_slice($rdap['status'], 0, 3)); + } + + if (!empty($rdap['nameservers']) && is_array($rdap['nameservers'])) { + $ns = array(); + foreach ($rdap['nameservers'] as $nameserver) { + if (!empty($nameserver['ldhName'])) { + $ns[] = strtolower($nameserver['ldhName']); + } + } + if (!empty($ns)) { + sort($ns); + $lines[] = 'Nameservers: ' . implode(', ', $ns); + } + } + + return implode("\n", $lines); +} - // Check each pattern to find a match - foreach ($patterns as $pattern) { - if (preg_match($pattern, $result, $matches)) { - $expireDate = trim($matches[1]); +// --- Legacy whois fallback (port-43 socket, for TLDs without RDAP) -------- + +function whoisSocketQuery($server, $query) { + $response = ''; + + $fp = @fsockopen($server, 43, $errno, $errstr, 5); + if (!$fp) { + return ''; + } + + stream_set_timeout($fp, 5); + fwrite($fp, $query . "\r\n"); + + while (!feof($fp)) { + $line = fgets($fp, 1024); + if ($line === false) { + break; + } + $response .= $line; + + // Sanity cap - expiry/registrar fields always appear well before this + if (strlen($response) > 32768) { break; } } + fclose($fp); + + return $response; +} - if ($expireDate) { - // Try parsing with known formats - foreach ($knownFormats as $format) { - $parsedDate = DateTime::createFromFormat($format, $expireDate); - if ($parsedDate && $parsedDate->format($format) === $expireDate) { - return $parsedDate->format('Y-m-d'); +function getDomainWhois($domain) { + $tld = substr(strrchr($domain, '.'), 1); + if (empty($tld)) { + return ''; + } + + // Ask IANA which whois server handles this TLD + $server = ''; + $iana_response = whoisSocketQuery('whois.iana.org', $tld); + if (preg_match('/^whois:\s*(\S+)/mi', $iana_response, $matches)) { + $server = $matches[1]; + } + if (empty($server)) { + return ''; + } + + // Verisign registries match nameservers too unless you use exact-match syntax + $query = in_array($tld, array('com', 'net')) ? "=$domain" : $domain; + + $result = whoisSocketQuery($server, $query); + + // Thin registries (.com/.net) refer to the registrar's whois - follow it once + if (preg_match('/Registrar WHOIS Server:\s*(\S+)/i', $result, $matches)) { + $referral = rtrim(trim($matches[1]), '/'); + $referral = preg_replace('#^r?whois://#i', '', $referral); + if (!empty($referral) && strcasecmp($referral, $server) !== 0) { + $referred_result = whoisSocketQuery($referral, $domain); + if (trim($referred_result) !== '') { + $result = $referred_result; } } + } + + return $result; +} - // If none of the formats matched, try to parse it directly - $parsedDate = date_create($expireDate); - if ($parsedDate) { +// --- Public API (names match current callers) ---------------------------- + +// getDnsRecords: native dns_get_record() for A/NS/MX/TXT + RDAP whois summary +function getDnsRecords($name) { + $records = array( + 'a' => '', + 'ns' => '', + 'mx' => '', + 'txt' => '', + 'whois' => '', + 'expire' => '' + ); + + // Only run if we think the domain is valid + if (!filter_var($name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) || !checkdnsrr($name, 'SOA')) { + return $records; + } + + // Anchored so we don't mangle domains that merely start with "www" + $domain = preg_replace('/^www\./i', '', strtolower(trim($name))); + + // A records + $a = @dns_get_record($domain, DNS_A); + if (is_array($a) && !empty($a)) { + $a_records = array_column($a, 'ip'); + sort($a_records); + $records['a'] = implode("\n", $a_records); + } + + // NS records + $ns = @dns_get_record($domain, DNS_NS); + if (is_array($ns) && !empty($ns)) { + $ns_records = array_column($ns, 'target'); + sort($ns_records); + $records['ns'] = implode("\n", $ns_records); + } + + // MX records - mimic dig +short output format ("10 mail.example.com") + $mx = @dns_get_record($domain, DNS_MX); + if (is_array($mx) && !empty($mx)) { + $mx_records = array(); + foreach ($mx as $record) { + $mx_records[] = $record['pri'] . ' ' . $record['target']; + } + sort($mx_records, SORT_NATURAL); + $records['mx'] = implode("\n", $mx_records); + } + + // TXT records + $txt = @dns_get_record($domain, DNS_TXT); + if (is_array($txt) && !empty($txt)) { + $txt_records = array_column($txt, 'txt'); + sort($txt_records); + $records['txt'] = implode("\n", $txt_records); + } + + // Registration data - RDAP first, legacy whois only if the TLD has no RDAP + $rdap = getDomainRdap($domain); + if ($rdap !== null) { + $records['whois'] = substr(getRdapSummary($rdap), 0, 254); + + $expires = getRdapEventDate($rdap, 'expiration'); + if (!empty($expires)) { + $parsed = date_create($expires); + if ($parsed) { + $records['expire'] = $parsed->format('Y-m-d'); + } + } + } else { + $whois_raw = getDomainWhois($domain); + if (!empty($whois_raw) && stripos($whois_raw, 'rate limit') === false) { + // Approximate the old `head -30 | sed 's/ //g'` + $lines = array_slice(explode("\n", $whois_raw), 0, 30); + $lines = array_map(function ($line) { + return preg_replace('/ +/', ' ', rtrim($line)); + }, $lines); + $records['whois'] = substr(trim(strip_tags(implode("\n", $lines))), 0, 254); + } + } + + return $records; +} + +function getDomainExpirationDate($domain) { + if (!filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) || !checkdnsrr($domain, 'SOA')) { + return null; + } + + $domain = preg_replace('/^www\./i', '', strtolower(trim($domain))); + + // RDAP: expiration is a structured field - no regex, no date-format guessing + $rdap = getDomainRdap($domain); + if ($rdap !== null) { + $expires = getRdapEventDate($rdap, 'expiration'); + if (!empty($expires)) { + $parsed = date_create($expires); + if ($parsed) { + return $parsed->format('Y-m-d'); + } + } + return null; // RDAP answered but had no expiry (rare) - trust it, don't re-query + } + + // Fallback for TLDs without RDAP: legacy whois parsing + $result = getDomainWhois($domain); + if (empty($result) || stripos($result, 'rate limit') !== false) { + return null; + } + + // Every expiry label seen in the wild, longest/most-specific first + $labels = array( + 'Registrar Registration Expiration Date', + 'Registry Expiry Date', + 'Expiration Date', + 'Expiration Time', + '\[Expires on\]', + 'Expires On', + 'Expiry Date', + 'Expire Date', + 'expire-date', + 'renewal date', + 'Valid Until', + 'paid-till', + 'validity', + 'renewal', + 'Expires', + 'expiry', + 'expire', + ); + + if (!preg_match('/(?:' . implode('|', $labels) . ')\s*:?\s+(.+)/i', $result, $matches)) { + return null; + } + $expireDate = trim($matches[1]); + + // Known date formats (roundtrip-checked to avoid d-m-Y vs Y-m-d ambiguity) + $knownFormats = array( + 'Y-m-d', + 'Y.m.d', + 'Y/m/d', + 'Ymd', + 'Y. m. d.', + 'Y-M-d.', + 'd-M-Y', + 'd-F-Y', + 'd-m-Y', + 'd.m.Y', + 'd/m/Y', + 'Y/m/d H:i:s', + 'Ymd H:i:s', + 'Y.m.d H:i:s', + 'Y-m-d H:i:s', + 'd-M-Y H:i:s', + 'd.m.Y H:i:s', + 'd/m/Y H:i:s', + 'd M Y H:i:s', + 'd/m/Y H:i:s T', + 'D M d H:i:s T Y', + 'D M d Y', + ); + + foreach ($knownFormats as $format) { + $parsedDate = DateTime::createFromFormat($format, $expireDate); + if ($parsedDate && $parsedDate->format($format) === $expireDate) { return $parsedDate->format('Y-m-d'); } } - - return null; // Return null if expiration date is not found + + // Fallback - handles ISO 8601 (2026-07-05T04:00:00Z) and most everything else + $parsedDate = date_create($expireDate); + if ($parsedDate) { + $year = (int) $parsedDate->format('Y'); + // Reject obviously bogus parses + if ($year >= 1995 && $year <= ((int) date('Y') + 100)) { + return $parsedDate->format('Y-m-d'); + } + } + + return null; }