mirror of
https://github.com/itflow-org/itflow
synced 2026-07-23 17:00:40 +00:00
470 lines
14 KiB
PHP
470 lines
14 KiB
PHP
<?php
|
|
|
|
|
|
// Used to automatically attempt to get SSL certificates as part of adding domains
|
|
// The logic for the fetch (sync) button on the client_certificates page is in ajax.php, and allows ports other than 443
|
|
function getSslCertificate($full_name)
|
|
{
|
|
|
|
// Parse host and port
|
|
$name = parse_url("//$full_name", PHP_URL_HOST);
|
|
$port = parse_url("//$full_name", PHP_URL_PORT);
|
|
|
|
// Default port
|
|
if (!$port) {
|
|
$port = "443";
|
|
}
|
|
|
|
$certificate = array();
|
|
$certificate['success'] = false;
|
|
|
|
// Only run if we think the domain is valid
|
|
if (!filter_var($name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
|
|
$certificate['expire'] = '';
|
|
$certificate['issued_by'] = '';
|
|
$certificate['public_key'] = '';
|
|
return $certificate;
|
|
}
|
|
|
|
// Get SSL/TSL certificate (using verify peer false to allow for self-signed certs) for domain on default port
|
|
$socket = "ssl://$name:$port";
|
|
$get = stream_context_create(array("ssl" => array("capture_peer_cert" => true, "verify_peer" => false,)));
|
|
$read = stream_socket_client($socket, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $get);
|
|
|
|
// If the socket connected
|
|
if ($read) {
|
|
$cert = stream_context_get_params($read);
|
|
$cert_public_key_obj = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
|
|
openssl_x509_export($cert['options']['ssl']['peer_certificate'], $export);
|
|
|
|
if ($cert_public_key_obj) {
|
|
$certificate['success'] = true;
|
|
$certificate['expire'] = date('Y-m-d', $cert_public_key_obj['validTo_time_t']);
|
|
$certificate['issued_by'] = strip_tags($cert_public_key_obj['issuer']['O']);
|
|
$certificate['public_key'] = $export;
|
|
}
|
|
}
|
|
|
|
return $certificate;
|
|
}
|
|
|
|
// ============================================================
|
|
// RDAP + native-DNS domain lookups (no shell_exec)
|
|
// Restored from commit d1e1609b; lost during the functions.php split.
|
|
// ============================================================
|
|
|
|
// --- RDAP helpers ---------------------------------------------------------
|
|
|
|
function rdapHttpGet($url, &$http_code = null) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, array(
|
|
CURLOPT_RETURNTRANSFER => 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 getRdapBaseUrl($tld) {
|
|
static $bootstrap = null;
|
|
|
|
if ($bootstrap === null) {
|
|
$cache_file = sys_get_temp_dir() . '/itflow_rdap_bootstrap.json';
|
|
|
|
if (is_readable($cache_file) && (time() - filemtime($cache_file)) < 604800) {
|
|
$bootstrap = json_decode(file_get_contents($cache_file), true);
|
|
}
|
|
|
|
if (empty($bootstrap['services'])) {
|
|
$raw = rdapHttpGet('https://data.iana.org/rdap/dns.json', $code);
|
|
if ($code === 200 && !empty($raw)) {
|
|
$decoded = json_decode($raw, true);
|
|
if (!empty($decoded['services'])) {
|
|
$bootstrap = $decoded;
|
|
@file_put_contents($cache_file, $raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($bootstrap['services'])) {
|
|
$bootstrap = array('services' => array()); // don't retry every call this run
|
|
}
|
|
}
|
|
|
|
foreach ($bootstrap['services'] as $service) {
|
|
// [0] = list of TLDs, [1] = list of base URLs
|
|
if (in_array($tld, $service[0])) {
|
|
return rtrim($service[1][0], '/') . '/';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
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 '';
|
|
}
|
|
|
|
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 '';
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// --- 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');
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|