From 3f3382bb898ce82c896114ba823fe2a658ce8994 Mon Sep 17 00:00:00 2001 From: Pinga <121483313+getpinga@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:56:26 +0200 Subject: [PATCH] Added limited WHOIS and RDAP on request of some ccTLDs --- rdap/rdap_limited.php | 3071 ++++++++++++++++++++++++++++++++ whois/port43/whois_limited.php | 427 +++++ 2 files changed, 3498 insertions(+) create mode 100644 rdap/rdap_limited.php create mode 100644 whois/port43/whois_limited.php diff --git a/rdap/rdap_limited.php b/rdap/rdap_limited.php new file mode 100644 index 0000000..5f16f2c --- /dev/null +++ b/rdap/rdap_limited.php @@ -0,0 +1,3071 @@ +withDriver($c['db_type']) + ->withHost($c['db_host']) + ->withPort($c['db_port']) + ->withDbName($c['db_database']) + ->withUsername($c['db_username']) + ->withPassword($c['db_password']) + ->withCharset('utf8mb4') +); + +// Create a Swoole HTTP server +$http = new Server('0.0.0.0', 7500); +$http->set([ + 'daemonize' => false, + 'log_file' => '/var/log/namingo/rdap_application.log', + 'log_level' => SWOOLE_LOG_INFO, + 'worker_num' => swoole_cpu_num() * 2, + 'pid_file' => '/var/run/rdap.pid', + 'max_request' => 1000, + 'dispatch_mode' => 1, + 'open_tcp_nodelay' => true, + 'max_conn' => 1024, + 'buffer_output_size' => 2 * 1024 * 1024, // 2MB + 'heartbeat_check_interval' => 60, + 'heartbeat_idle_time' => 600, // 10 minutes + 'package_max_length' => 2 * 1024 * 1024, // 2MB + 'reload_async' => true, + 'http_compression' => true +]); + +$rateLimiter = new Rately(); +$log->info('server started.'); + +// Handle incoming HTTP requests +$http->on('request', function ($request, $response) use ($c, $pool, $log, $rateLimiter) { + // Get a PDO connection from the pool + try { + $pdo = $pool->get(); + if (!$pdo) { + throw new PDOException("Failed to retrieve a connection from Swoole PDOPool."); + } + } catch (PDOException $e) { + $log->alert("Swoole PDO Pool failed: " . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(500); + $response->end(json_encode(['error' => 'Database failure. Please try again later.'])); + } + + $remoteAddr = $request->server['remote_addr']; + if (!isIpWhitelisted($remoteAddr, $pdo)) { + if (($c['rately'] == true) && ($rateLimiter->isRateLimited('rdap', $remoteAddr, $c['limit'], $c['period']))) { + $log->error('rate limit exceeded for ' . $remoteAddr); + $response->header('Content-Type', 'application/json'); + $response->status(429); + $response->end(json_encode(['error' => 'Rate limit exceeded. Please try again later.'])); + } + } + + try { + // Extract the request path + $requestPath = $request->server['request_uri']; + + // Handle domain query + if (preg_match('#^/domain/([^/?]+)#', $requestPath, $matches)) { + $domainName = $matches[1]; + handleDomainQuery($request, $response, $pdo, $domainName, $c, $log); + } + // Handle entity (contacts) query + elseif (preg_match('#^/entity/([^/?]+)#', $requestPath, $matches)) { + $entityHandle = $matches[1]; + handleEntityQuery($request, $response, $pdo, $entityHandle, $c, $log); + } + // Handle nameserver query + elseif (preg_match('#^/nameserver/([^/?]+)#', $requestPath, $matches)) { + $nameserverHandle = $matches[1]; + handleNameserverQuery($request, $response, $pdo, $nameserverHandle, $c, $log); + } + // Handle domain search query + elseif ($requestPath === '/domains') { + if (isset($request->server['query_string'])) { + parse_str($request->server['query_string'], $queryParams); + + if (isset($queryParams['name'])) { + $searchPattern = $queryParams['name']; + handleDomainSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'name'); + } elseif (isset($queryParams['nsLdhName'])) { + $searchPattern = $queryParams['nsLdhName']; + handleDomainSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'nsLdhName'); + } elseif (isset($queryParams['nsIp'])) { + $searchPattern = $queryParams['nsIp']; + handleDomainSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'nsIp'); + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } + // Handle nameserver search query + elseif ($requestPath === '/nameservers') { + if (isset($request->server['query_string'])) { + parse_str($request->server['query_string'], $queryParams); + + if (isset($queryParams['name'])) { + $searchPattern = $queryParams['name']; + handleNameserverSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'name'); + } elseif (isset($queryParams['ip'])) { + $searchPattern = $queryParams['ip']; + handleNameserverSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'ip'); + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } + // Handle entity search query + elseif ($requestPath === '/entities') { + if (isset($request->server['query_string'])) { + parse_str($request->server['query_string'], $queryParams); + + if (isset($queryParams['fn'])) { + $searchPattern = $queryParams['fn']; + handleEntitySearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'fn'); + } elseif (isset($queryParams['handle'])) { + $searchPattern = $queryParams['handle']; + handleEntitySearchQuery($request, $response, $pdo, $searchPattern, $c, $log, 'handle'); + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['error' => 'Object not found'])); + } + } + // Handle help query + elseif ($requestPath === '/help') { + handleHelpQuery($request, $response, $pdo, $c); + } + else { + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode(['errorCode' => 404,'title' => 'Not Found','error' => 'Endpoint not found'])); + } + } catch (PDOException $e) { + // Handle database exceptions + $log->error('Database error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + } catch (Throwable $e) { + // Catch any other exceptions or errors + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Error:' => $e->getMessage()])); + } finally { + // Return the connection to the pool + $pool->put($pdo); + } + +}); + +// Start the server +$http->start(); + +function handleDomainQuery($request, $response, $pdo, $domainName, $c, $log) { + // Extract and validate the domain name from the request + $domain = urldecode($domainName); + $domain = trim($domain); + + // Empty domain check + if (!$domain) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter a domain name'])); + return; + } + + // Check domain length + if (strlen($domain) > 68) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name is too long'])); + return; + } + + // Convert to Punycode if the domain is not in ASCII + if (!mb_detect_encoding($domain, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain conversion to Punycode failed'])); + return; + } else { + $domain = $convertedDomain; + } + } + + // Check for prohibited patterns in domain names + if (!preg_match('/^(?:(xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){1,3}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $domain)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name invalid format'])); + return; + } + + // Extract TLD from the domain + $parts = explode('.', $domain); + + // Handle multi-segment TLDs (e.g., co.uk, ngo.us, etc.) + if (count($parts) > 2) { + $tld = "." . $parts[count($parts) - 2] . "." . $parts[count($parts) - 1]; + } else { + $tld = "." . end($parts); + } + + // Check if the TLD exists in the domain_tld table + $stmtTLD = $pdo->prepare("SELECT COUNT(*) FROM domain_tld WHERE tld = :tld"); + $stmtTLD->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtTLD->execute(); + $tldExists = $stmtTLD->fetchColumn(); + + if (!$tldExists) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Invalid TLD. Please search only allowed TLDs'])); + return; + } + + // Check if domain is reserved + $stmtReserved = $pdo->prepare("SELECT id FROM reserved_domain_names WHERE name = ? LIMIT 1"); + $stmtReserved->execute([$parts[0]]); + $domain_already_reserved = $stmtReserved->fetchColumn(); + + if ($domain_already_reserved) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name is reserved or restricted'])); + return; + } + + // Fetch the IDN regex for the given TLD + $stmtRegex = $pdo->prepare("SELECT idn_table FROM domain_tld WHERE tld = :tld"); + $stmtRegex->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtRegex->execute(); + $idnRegex = $stmtRegex->fetchColumn(); + + if (!$idnRegex) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Failed to fetch domain IDN table'])); + return; + } + + // Check for invalid characters using fetched regex + if (strpos($parts[0], 'xn--') === 0) { + $label = idn_to_utf8($parts[0], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $label = $parts[0]; + } + if (!preg_match($idnRegex, $label)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name invalid IDN characters'])); + return; + } + + // Perform the RDAP lookup + try { + // Query 1: Get domain details + $stmt1 = $pdo->prepare("SELECT * FROM domain WHERE name = :domain"); + $stmt1->bindParam(':domain', $domain, PDO::PARAM_STR); + $stmt1->execute(); + $domainDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + + // Check if the domain exists + if (!$domainDetails) { + // Domain not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested domain was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + $domainDetails['crdate'] = (new DateTime($domainDetails['crdate']))->format('Y-m-d\TH:i:s.v\Z'); + $domainDetails['exdate'] = (new DateTime($domainDetails['exdate']))->format('Y-m-d\TH:i:s.v\Z'); + + // Query 2: Get status details + $stmt2 = $pdo->prepare("SELECT status FROM domain_status WHERE domain_id = :domain_id"); + $stmt2->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $statuses = $stmt2->fetchAll(PDO::FETCH_COLUMN, 0); + + // Add rgpstatus to statuses if it's not empty + if (!empty($domainDetails['rgpstatus'])) { + $statuses[] = $domainDetails['rgpstatus']; + } + + // If statuses array is empty, add 'active' to it + if (empty($statuses)) { + $statuses[] = 'active'; + } + + $statuses = mapStatuses($statuses); + + // Query: Get DNSSEC details + $stmt2a = $pdo->prepare("SELECT interface FROM secdns WHERE domain_id = :domain_id"); + $stmt2a->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt2a->execute(); + $isDelegationSigned = $stmt2a->fetchColumn() > 0; + + $stmt2b = $pdo->prepare("SELECT secure FROM domain_tld WHERE tld = :tld"); + $stmt2b->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmt2b->execute(); + $isZoneSigned = ($stmt2b->fetchColumn() == 1); + + // Query 3: Get registrar details + $stmt3 = $pdo->prepare("SELECT id,name,iana_id,whois_server,rdap_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"); + $stmt3->bindParam(':clid', $domainDetails['clid'], PDO::PARAM_INT); + $stmt3->execute(); + $registrarDetails = $stmt3->fetch(PDO::FETCH_ASSOC); + + // Query: Get registrar abuse details + $stmt3a = $pdo->prepare("SELECT first_name,last_name FROM registrar_contact WHERE registrar_id = :clid AND type = 'abuse'"); + $stmt3a->bindParam(':clid', $domainDetails['clid'], PDO::PARAM_INT); + $stmt3a->execute(); + $registrarAbuseDetails = $stmt3a->fetch(PDO::FETCH_ASSOC); + + // Query 4: Get registrant details + $stmt4 = $pdo->prepare("SELECT contact.id,contact_postalInfo.name,contact_postalInfo.org,contact_postalInfo.street1,contact_postalInfo.street2,contact_postalInfo.street3,contact_postalInfo.city,contact_postalInfo.sp,contact_postalInfo.pc,contact_postalInfo.cc,contact.voice,contact.voice_x,contact.fax,contact.fax_x,contact.email,contact.disclose_voice,contact.disclose_fax,contact.disclose_email,contact_postalInfo.type,contact_postalInfo.disclose_name_int,contact_postalInfo.disclose_name_loc,contact_postalInfo.disclose_org_int,contact_postalInfo.disclose_org_loc,contact_postalInfo.disclose_addr_int,contact_postalInfo.disclose_addr_loc FROM contact,contact_postalInfo WHERE contact.id=:registrant AND contact_postalInfo.contact_id=contact.id"); + $stmt4->bindParam(':registrant', $domainDetails['registrant'], PDO::PARAM_INT); + $stmt4->execute(); + $registrantDetails = $stmt4->fetch(PDO::FETCH_ASSOC); + + // Query 5: Get admin, billing and tech contacts + $stmtMap = $pdo->prepare("SELECT contact_id, type FROM domain_contact_map WHERE domain_id = :domain_id"); + $stmtMap->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmtMap->execute(); + $contactMap = $stmtMap->fetchAll(PDO::FETCH_ASSOC); + + $adminDetails = []; + $techDetails = []; + $billingDetails = []; + + foreach ($contactMap as $map) { + $stmtDetails = $pdo->prepare("SELECT contact.id, contact_postalInfo.name, contact_postalInfo.org, contact_postalInfo.street1, contact_postalInfo.street2, contact_postalInfo.street3, contact_postalInfo.city, contact_postalInfo.sp, contact_postalInfo.pc, contact_postalInfo.cc, contact.voice, contact.voice_x, contact.fax, contact.fax_x, contact.email, contact.disclose_voice,contact.disclose_fax,contact.disclose_email,contact_postalInfo.type,contact_postalInfo.disclose_name_int,contact_postalInfo.disclose_name_loc,contact_postalInfo.disclose_org_int,contact_postalInfo.disclose_org_loc,contact_postalInfo.disclose_addr_int,contact_postalInfo.disclose_addr_loc FROM contact, contact_postalInfo WHERE contact.id = :contact_id AND contact_postalInfo.contact_id = contact.id"); + $stmtDetails->bindParam(':contact_id', $map['contact_id'], PDO::PARAM_INT); + $stmtDetails->execute(); + + $contactDetails = $stmtDetails->fetch(PDO::FETCH_ASSOC); + + switch ($map['type']) { + case 'admin': + $adminDetails[] = $contactDetails; + break; + case 'tech': + $techDetails[] = $contactDetails; + break; + case 'billing': + $billingDetails[] = $contactDetails; + break; + } + } + + // Query 6: Get nameservers + $stmt6 = $pdo->prepare(" + SELECT host.name, host.id as host_id + FROM domain_host_map, host + WHERE domain_host_map.domain_id = :domain_id + AND domain_host_map.host_id = host.id + "); + $stmt6->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt6->execute(); + $nameservers = $stmt6->fetchAll(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'registration', 'eventDate' => $domainDetails['crdate']], + ['eventAction' => 'expiration', 'eventDate' => $domainDetails['exdate']], + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + // Check if domain last update is set and not empty + if (isset($domainDetails['lastupdate']) && !empty($domainDetails['lastupdate'])) { + $updateDateTime = new DateTime($domainDetails['lastupdate']); + $events[] = [ + 'eventAction' => 'last changed', + 'eventDate' => $updateDateTime->format('Y-m-d\TH:i:s.v\Z') + ]; + } + + // Check if domain transfer date is set and not empty + if (isset($domainDetails['trdate']) && !empty($domainDetails['trdate'])) { + $transferDateTime = new DateTime($domainDetails['trdate']); + $events[] = [ + 'eventAction' => 'transfer', + 'eventDate' => $transferDateTime->format('Y-m-d\TH:i:s.v\Z') + ]; + } + + $abuseContactName = ($registrarAbuseDetails) ? $registrarAbuseDetails['first_name'] . ' ' . $registrarAbuseDetails['last_name'] : ''; + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'objectClassName' => 'domain', + 'entities' => array_merge( + [ + [ + 'objectClassName' => 'entity', + 'entities' => [ + [ + 'objectClassName' => 'entity', + 'roles' => ["abuse"], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + //["fn", new stdClass(), "text", $abuseContactName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $registrarDetails['abuse_phone']], + ["email", new stdClass(), "text", $registrarDetails['abuse_email']] + ] + ], + ], + ], + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + "links" => [ + [ + "href" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "value" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "rel" => "self", + "type" => "application/rdap+json" + ] + ], +/* "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], */ + "remarks" => [ + [ + "description" => ["This record contains only a summary. For detailed information, please submit a query specifically for this object."], + "title" => "Incomplete Data", + "type" => "object truncated due to authorization" + ] + ], + "roles" => ["registrar"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $registrarDetails['name']] + ] + ], + ], + ], +/* !$c['minimum_data'] ? [ + [ + mapContactToVCard($registrantDetails, 'registrant', $c) + ], + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'admin', $c); + }, $adminDetails), + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'tech', $c); + }, $techDetails), + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'billing', $c); + }, $billingDetails) + ] : [] */ + ), + 'events' => $events, + //'handle' => 'D' . $domainDetails['id'] . '-' . $c['roid'] . '', + 'ldhName' => $domain, + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/domain/' . $domain, + 'value' => $c['rdap_url'] . '/domain/' . $domain, + 'rel' => 'self', + 'type' => 'application/rdap+json', + ], + [ + 'href' => $registrarDetails['rdap_server'] . 'domain/' . $domain, + 'value' => $registrarDetails['rdap_server'] . 'domain/' . $domain, + 'rel' => 'related', + 'type' => 'application/rdap+json', + ] + ], + 'nameservers' => array_map(function ($nameserverDetails) use ($c) { + return [ + 'objectClassName' => 'nameserver', + 'handle' => 'H' . $nameserverDetails['host_id'] . '-' . $c['roid'] . '', + 'ldhName' => $nameserverDetails['name'], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/nameserver/' . $nameserverDetails['name'], + 'value' => $c['rdap_url'] . '/nameserver/' . $nameserverDetails['name'], + 'rel' => 'self', + 'type' => 'application/rdap+json', + ], + ], + 'remarks' => [ + [ + "description" => [ + "This record contains only a brief summary. To access the full details, please initiate a specific query targeting this entity." + ], + "title" => "Incomplete Data", + "type" => "object truncated due to authorization" + ], + ], + ]; + }, $nameservers), + "secureDNS" => [ + "delegationSigned" => $isDelegationSigned, + "zoneSigned" => $isZoneSigned + ], + 'status' => $statuses, + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleEntityQuery($request, $response, $pdo, $entityHandle, $c, $log) { + // Extract and validate the entity handle from the request + $entity = trim($entityHandle); + + // Empty entity check + if (!$entity) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter an entity'])); + return; + } + + // Check for prohibited patterns in RDAP entity handle + if (!preg_match("/^[A-Za-z0-9]+$/", $entity)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Entity handle invalid format'])); + return; + } + + // Perform the RDAP lookup + try { + // Validate $entity to ensure it is numeric and contains only digits + if (!is_numeric($entity)) { + // Return a 404 response if $entity is not a purely numeric string + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested entity was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + // Query 1: Get registrar details + $stmt1 = $pdo->prepare("SELECT id,name,clid,iana_id,whois_server,rdap_server,url,email,abuse_email,abuse_phone FROM registrar WHERE iana_id = :iana_id"); + $stmt1->bindParam(':iana_id', $entity, PDO::PARAM_INT); + $stmt1->execute(); + $registrarDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + + // Check if the first query returned a result + if (!$registrarDetails) { + // Query 2: Get registrar details by id as a fallback for ccTLDs without iana_id + $stmt2 = $pdo->prepare("SELECT id, name, clid, iana_id, whois_server, rdap_server, url, email, abuse_email, abuse_phone FROM registrar WHERE id = :id"); + $stmt2->bindParam(':id', $entity, PDO::PARAM_INT); + $stmt2->execute(); + $registrarDetails = $stmt2->fetch(PDO::FETCH_ASSOC); + } + + // Check if the entity exists + if (!$registrarDetails) { + // Entity not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested entity was not found in the RDAP database.', + ])); + // Close the connection + $pdo = null; + return; + } + + // Query 2: Fetch all contact types for a registrar + $stmt2 = $pdo->prepare("SELECT type, first_name, last_name, voice, email FROM registrar_contact WHERE registrar_id = :clid"); + $stmt2->bindParam(':clid', $registrarDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $contacts = $stmt2->fetchAll(PDO::FETCH_ASSOC); + + // Query 3: Get registrar abuse details + $stmt3 = $pdo->prepare("SELECT org,street1,street2,city,sp,pc,cc FROM registrar_contact WHERE registrar_id = :clid AND type = 'owner'"); + $stmt3->bindParam(':clid', $registrarDetails['id'], PDO::PARAM_INT); + $stmt3->execute(); + $registrarContact = $stmt3->fetch(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + // Initialize an array to hold entity blocks + $entityBlocks = []; + // Define an array of allowed contact types + $allowedTypes = ['owner', 'tech', 'abuse']; + + foreach ($contacts as $contact) { + // Check if the contact type is one of the allowed types + if (in_array($contact['type'], $allowedTypes)) { + // Build the full name + $fullName = $contact['first_name'] . ' ' . $contact['last_name']; + + // Create an entity block for each allowed contact type + $entityBlock = [ + 'objectClassName' => 'entity', + 'roles' => [$contact['type']], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $fullName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $contact['voice']], + ["email", new stdClass(), "text", $contact['email']] + ] + ], + ]; + + // Add the entity block to the array + $entityBlocks[] = $entityBlock; + } + } + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'objectClassName' => 'entity', + 'entities' => $entityBlocks, + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + 'events' => $events, + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/entity/' . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + 'rel' => 'self', + 'type' => 'application/rdap+json', + ] + ], + "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], + "roles" => ["registrar"], + "status" => ["active"], + 'vcardArray' => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), 'text', $registrarContact['org']], + ["adr", [ + "", // Post office box + $registrarContact['street1'], // Extended address + $registrarContact['street2'], // Street address + $registrarContact['city'], // Locality + $registrarContact['sp'], // Region + $registrarContact['pc'], // Postal code + $registrarContact['cc'] // Country name + ]], + ["email", $registrarDetails['email']], + ] + ], + "notices" => [ + [ + "description" => [ + "Access to RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleNameserverQuery($request, $response, $pdo, $nameserverHandle, $c, $log) { + // Extract and validate the nameserver handle from the request + $ns = urldecode($nameserverHandle); + $ns = trim($ns); + + // Empty nameserver check + if (!$ns) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter a nameserver'])); + return; + } + + // Check nameserver length + $labels = explode('.', $ns); + $validLengths = array_map(function ($label) { + return strlen($label) <= 63; + }, $labels); + + if (strlen($ns) > 253 || in_array(false, $validLengths, true)) { + // The nameserver format is invalid due to length + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Nameserver is too long'])); + return; + } + + // Convert to Punycode if the host is not in ASCII + if (!mb_detect_encoding($ns, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($ns, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Host conversion to Punycode failed'])); + return; + } else { + $ns = $convertedDomain; + } + } + + // Check for prohibited patterns in nameserver + if (!preg_match('/^((xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){2,}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $ns)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Nameserver invalid format'])); + return; + } + + // Extract TLD from the domain + $parts = explode('.', $ns); + $tld = "." . end($parts); + + // Perform the RDAP lookup + try { + // Query 1: Get nameserver details + $stmt1 = $pdo->prepare("SELECT id,name,clid FROM host WHERE name = :ns"); + $stmt1->bindParam(':ns', $ns, PDO::PARAM_STR); + $stmt1->execute(); + $hostDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + + // Check if the nameserver exists + if (!$hostDetails) { + // Nameserver not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested nameserver was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + // Query 2: Get status details + $stmt2 = $pdo->prepare("SELECT status FROM host_status WHERE host_id = :host_id"); + $stmt2->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $statuses = $stmt2->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 2a: Get associated status details + $stmt2a = $pdo->prepare("SELECT domain_id FROM domain_host_map WHERE host_id = :host_id"); + $stmt2a->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt2a->execute(); + $associated = $stmt2a->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 3: Get IP details + $stmt3 = $pdo->prepare("SELECT addr,ip FROM host_addr WHERE host_id = :host_id"); + $stmt3->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt3->execute(); + $ipDetails = $stmt3->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 4: Get registrar details + $stmt4 = $pdo->prepare("SELECT id,name,iana_id,whois_server,rdap_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"); + $stmt4->bindParam(':clid', $hostDetails['clid'], PDO::PARAM_INT); + $stmt4->execute(); + $registrarDetails = $stmt4->fetch(PDO::FETCH_ASSOC); + + // Query 5: Get registrar abuse details + $stmt5 = $pdo->prepare("SELECT first_name,last_name FROM registrar_contact WHERE registrar_id = :clid AND type = 'abuse'"); + $stmt5->bindParam(':clid', $hostDetails['clid'], PDO::PARAM_INT); + $stmt5->execute(); + $registrarAbuseDetails = $stmt5->fetch(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + $abuseContactName = ($registrarAbuseDetails) ? $registrarAbuseDetails['first_name'] . ' ' . $registrarAbuseDetails['last_name'] : ''; + + // Build the 'ipAddresses' structure + $ipAddresses = array_reduce($ipDetails, function ($carry, $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $carry['v4'][] = $ip; + } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $carry['v6'][] = $ip; + } + return $carry; + }, ['v4' => [], 'v6' => []]); // Initialize with 'v4' and 'v6' keys + + // Check if both v4 and v6 are empty, then set to empty object for JSON encoding + if (empty($ipAddresses['v4']) && empty($ipAddresses['v6'])) { + $ipAddresses = new stdClass(); // This will encode to {} in JSON + } + + // If there are associated domains, add 'associated' to the statuses + if (!empty($associated)) { + $statuses[] = 'associated'; + } + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'objectClassName' => 'nameserver', + 'entities' => array_merge( + [ + [ + 'objectClassName' => 'entity', + 'entities' => [ + [ + 'objectClassName' => 'entity', + 'roles' => ["abuse"], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $abuseContactName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $registrarDetails['abuse_phone']], + ["email", new stdClass(), "text", $registrarDetails['abuse_email']] + ] + ], + ], + ], + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + "links" => [ + [ + "href" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "value" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "rel" => "self", + "type" => "application/rdap+json" + ] + ], + "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], + "remarks" => [ + [ + "description" => ["This record contains only a summary. For detailed information, please submit a query specifically for this object."], + "title" => "Incomplete Data", + "type" => "object truncated due to authorization" + ] + ], + "roles" => ["registrar"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $registrarDetails['name']] + ] + ], + ], + ], + ), + 'handle' => 'H' . $hostDetails['id'] . '-' . $c['roid'] . '', + 'ipAddresses' => $ipAddresses, + 'events' => $events, + 'ldhName' => $hostDetails['name'], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/nameserver/' . $hostDetails['name'], + 'rel' => 'self', + 'type' => 'application/rdap+json', + ] + ], + 'status' => $statuses, + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleDomainSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, $searchType) { + // Extract and validate the domain name from the request + $domain = urldecode($searchPattern); + $domain = trim($domain); + + // Empty domain check + if (!$domain) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter a domain name'])); + return; + } + + switch ($searchType) { + case 'name': + // Search by domain name + break; + case 'nsLdhName': + // Search by nameserver LDH name + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested nameserver was not found in the RDAP database.', + ])); + // Close the connection + $pdo = null; + return; + case 'nsIp': + // Search by nameserver IP address + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested IP was not found in the RDAP database.', + ])); + // Close the connection + $pdo = null; + return; + } + + // Check domain length + if (strlen($domain) > 68) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name is too long'])); + return; + } + + // Convert to Punycode if the domain is not in ASCII + if (!mb_detect_encoding($domain, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain conversion to Punycode failed'])); + return; + } else { + $domain = $convertedDomain; + } + } + + // Check for prohibited patterns in domain names + if (!preg_match('/^(?:(xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){1,3}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $domain)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name invalid format'])); + return; + } + + // Extract TLD from the domain + $parts = explode('.', $domain); + $tld = "." . end($parts); + + // Check if the TLD exists in the domain_tld table + $stmtTLD = $pdo->prepare("SELECT COUNT(*) FROM domain_tld WHERE tld = :tld"); + $stmtTLD->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtTLD->execute(); + $tldExists = $stmtTLD->fetchColumn(); + + if (!$tldExists) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Invalid TLD. Please search only allowed TLDs'])); + return; + } + + // Check if domain is reserved + $stmtReserved = $pdo->prepare("SELECT id FROM reserved_domain_names WHERE name = ? LIMIT 1"); + $stmtReserved->execute([$parts[0]]); + $domain_already_reserved = $stmtReserved->fetchColumn(); + + if ($domain_already_reserved) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name is reserved or restricted'])); + return; + } + + // Fetch the IDN regex for the given TLD + $stmtRegex = $pdo->prepare("SELECT idn_table FROM domain_tld WHERE tld = :tld"); + $stmtRegex->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtRegex->execute(); + $idnRegex = $stmtRegex->fetchColumn(); + + if (!$idnRegex) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Failed to fetch domain IDN table'])); + return; + } + + // Check for invalid characters using fetched regex + if (strpos($parts[0], 'xn--') === 0) { + $label = idn_to_utf8($parts[0], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $label = $parts[0]; + } + if (!preg_match($idnRegex, $label)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Domain name invalid IDN characters'])); + return; + } + + // Perform the RDAP lookup + try { + // Query 1: Get domain details + $stmt1 = $pdo->prepare("SELECT * FROM domain WHERE name = :domain"); + $stmt1->bindParam(':domain', $domain, PDO::PARAM_STR); + $stmt1->execute(); + $domainDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + + // Check if the domain exists + if (!$domainDetails) { + // Domain not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested domain was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + $domainDetails['crdate'] = (new DateTime($domainDetails['crdate']))->format('Y-m-d\TH:i:s.v\Z'); + $domainDetails['exdate'] = (new DateTime($domainDetails['exdate']))->format('Y-m-d\TH:i:s.v\Z'); + + // Query 2: Get status details + $stmt2 = $pdo->prepare("SELECT status FROM domain_status WHERE domain_id = :domain_id"); + $stmt2->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $statuses = $stmt2->fetchAll(PDO::FETCH_COLUMN, 0); + + // Add rgpstatus to statuses if it's not empty + //if (!empty($domainDetails['rgpstatus'])) { + //$statuses[] = $domainDetails['rgpstatus']; + //} + + // If statuses array is empty, add 'active' to it + if (empty($statuses)) { + $statuses[] = 'active'; + } + + // Query: Get DNSSEC details + $stmt2a = $pdo->prepare("SELECT interface FROM secdns WHERE domain_id = :domain_id"); + $stmt2a->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt2a->execute(); + $isDelegationSigned = $stmt2a->fetchColumn() > 0; + + $stmt2b = $pdo->prepare("SELECT secure FROM domain_tld WHERE tld = :tld"); + $stmt2b->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmt2b->execute(); + $isZoneSigned = ($stmt2b->fetchColumn() == 1); + + // Query 3: Get registrar details + $stmt3 = $pdo->prepare("SELECT id,name,iana_id,whois_server,rdap_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"); + $stmt3->bindParam(':clid', $domainDetails['clid'], PDO::PARAM_INT); + $stmt3->execute(); + $registrarDetails = $stmt3->fetch(PDO::FETCH_ASSOC); + + // Query: Get registrar abuse details + $stmt3a = $pdo->prepare("SELECT first_name,last_name FROM registrar_contact WHERE registrar_id = :clid AND type = 'abuse'"); + $stmt3a->bindParam(':clid', $domainDetails['clid'], PDO::PARAM_INT); + $stmt3a->execute(); + $registrarAbuseDetails = $stmt3a->fetch(PDO::FETCH_ASSOC); + + // Query 4: Get registrant details + $stmt4 = $pdo->prepare("SELECT contact.identifier,contact_postalInfo.name,contact_postalInfo.org,contact_postalInfo.street1,contact_postalInfo.street2,contact_postalInfo.street3,contact_postalInfo.city,contact_postalInfo.sp,contact_postalInfo.pc,contact_postalInfo.cc,contact.voice,contact.voice_x,contact.fax,contact.fax_x,contact.email,contact.disclose_voice,contact.disclose_fax,contact.disclose_email,contact_postalInfo.type,contact_postalInfo.disclose_name_int,contact_postalInfo.disclose_name_loc,contact_postalInfo.disclose_org_int,contact_postalInfo.disclose_org_loc,contact_postalInfo.disclose_addr_int,contact_postalInfo.disclose_addr_loc FROM contact,contact_postalInfo WHERE contact.id=:registrant AND contact_postalInfo.contact_id=contact.id"); + $stmt4->bindParam(':registrant', $domainDetails['registrant'], PDO::PARAM_INT); + $stmt4->execute(); + $registrantDetails = $stmt4->fetch(PDO::FETCH_ASSOC); + + // Query 5: Get admin, billing and tech contacts + $stmtMap = $pdo->prepare("SELECT contact_id, type FROM domain_contact_map WHERE domain_id = :domain_id"); + $stmtMap->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmtMap->execute(); + $contactMap = $stmtMap->fetchAll(PDO::FETCH_ASSOC); + + $adminDetails = []; + $techDetails = []; + $billingDetails = []; + + foreach ($contactMap as $map) { + $stmtDetails = $pdo->prepare("SELECT contact.identifier, contact_postalInfo.name, contact_postalInfo.org, contact_postalInfo.street1, contact_postalInfo.street2, contact_postalInfo.street3, contact_postalInfo.city, contact_postalInfo.sp, contact_postalInfo.pc, contact_postalInfo.cc, contact.voice, contact.voice_x, contact.fax, contact.fax_x, contact.email,contact.disclose_voice,contact.disclose_fax,contact.disclose_email,contact_postalInfo.type,contact_postalInfo.disclose_name_int,contact_postalInfo.disclose_name_loc,contact_postalInfo.disclose_org_int,contact_postalInfo.disclose_org_loc,contact_postalInfo.disclose_addr_int,contact_postalInfo.disclose_addr_loc FROM contact, contact_postalInfo WHERE contact.id = :contact_id AND contact_postalInfo.contact_id = contact.id"); + $stmtDetails->bindParam(':contact_id', $map['contact_id'], PDO::PARAM_INT); + $stmtDetails->execute(); + + $contactDetails = $stmtDetails->fetch(PDO::FETCH_ASSOC); + + switch ($map['type']) { + case 'admin': + $adminDetails[] = $contactDetails; + break; + case 'tech': + $techDetails[] = $contactDetails; + break; + case 'billing': + $billingDetails[] = $contactDetails; + break; + } + } + + // Query 6: Get nameservers + $stmt6 = $pdo->prepare(" + SELECT host.name, host.id as host_id + FROM domain_host_map, host + WHERE domain_host_map.domain_id = :domain_id + AND domain_host_map.host_id = host.id + "); + $stmt6->bindParam(':domain_id', $domainDetails['id'], PDO::PARAM_INT); + $stmt6->execute(); + $nameservers = $stmt6->fetchAll(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'registration', 'eventDate' => $domainDetails['crdate']], + ['eventAction' => 'expiration', 'eventDate' => $domainDetails['exdate']], + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + // Check if domain last update is set and not empty + if (isset($domainDetails['lastupdate']) && !empty($domainDetails['lastupdate'])) { + $updateDateTime = new DateTime($domainDetails['lastupdate']); + $events[] = [ + 'eventAction' => 'last changed', + 'eventDate' => $updateDateTime->format('Y-m-d\TH:i:s.v\Z') + ]; + } + + // Check if domain transfer date is set and not empty + if (isset($domainDetails['trdate']) && !empty($domainDetails['trdate'])) { + $transferDateTime = new DateTime($domainDetails['trdate']); + $events[] = [ + 'eventAction' => 'transfer', + 'eventDate' => $transferDateTime->format('Y-m-d\TH:i:s.v\Z') + ]; + } + + $abuseContactName = ($registrarAbuseDetails) ? $registrarAbuseDetails['first_name'] . ' ' . $registrarAbuseDetails['last_name'] : ''; + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'domainSearchResults' => [ + [ + 'objectClassName' => 'domain', + 'entities' => array_merge( + [ + [ + 'objectClassName' => 'entity', + 'entities' => [ + [ + 'objectClassName' => 'entity', + 'roles' => ["abuse"], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $abuseContactName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $registrarDetails['abuse_phone']], + ["email", new stdClass(), "text", $registrarDetails['abuse_email']] + ] + ], + ], + ], + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + "links" => [ + [ + "href" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "value" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "rel" => "self", + "type" => "application/rdap+json" + ] + ], + "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], + "remarks" => [ + [ + "description" => ["This record contains only a summary. For detailed information, please submit a query specifically for this object."], + "title" => "Incomplete Data", + "type" => "object truncated due to authorization" + ] + ], + "roles" => ["registrar"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $registrarDetails['name']] + ] + ], + ], + ], + [ + mapContactToVCard($registrantDetails, 'registrant', $c) + ], + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'admin', $c); + }, $adminDetails), + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'tech', $c); + }, $techDetails), + array_map(function ($contact) use ($c) { + return mapContactToVCard($contact, 'billing', $c); + }, $billingDetails) + ), + 'events' => $events, + 'handle' => 'D' . $domainDetails['id'] . '-' . $c['roid'] . '', + 'ldhName' => $domain, + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/domain/' . $domain, + 'rel' => 'self', + 'type' => 'application/rdap+json', + ], + [ + 'href' => $registrarDetails['rdap_server'] . 'domain/' . $domain, + 'rel' => 'related', + 'type' => 'application/rdap+json', + ] + ], + 'nameservers' => array_map(function ($nameserverDetails) use ($c) { + return [ + 'objectClassName' => 'nameserver', + 'handle' => 'H' . $nameserverDetails['host_id'] . '-' . $c['roid'] . '', + 'ldhName' => $nameserverDetails['name'], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/nameserver/' . $nameserverDetails['name'], + 'rel' => 'self', + 'type' => 'application/rdap+json', + ], + ], + 'remarks' => [ + [ + "description" => [ + "This record contains only a brief summary. To access the full details, please initiate a specific query targeting this entity." + ], + "title" => "Incomplete Data", + "type" => "object truncated due to authorization" + ], + ], + ]; + }, $nameservers), + "secureDNS" => [ + "delegationSigned" => $isDelegationSigned, + "zoneSigned" => $isZoneSigned + ], + 'status' => $statuses, + ], + ], + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleNameserverSearchQuery($request, $response, $pdo, $searchPattern, $c, $log, $searchType) { + // Extract and validate the nameserver handle from the request + $ns = urldecode($searchPattern); + $ns = trim($ns); + + // Perform the RDAP lookup + try { + // Query 1: Get nameserver details + switch ($searchType) { + case 'name': + // Search by nameserver + + // Empty nameserver check + if (!$ns) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter a nameserver'])); + return; + } + + // Check nameserver length + $labels = explode('.', $ns); + $validLengths = array_map(function ($label) { + return strlen($label) <= 63; + }, $labels); + + if (strlen($ns) > 253 || in_array(false, $validLengths, true)) { + // The nameserver format is invalid due to length + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Nameserver is too long'])); + return; + } + + // Convert to Punycode if the host is not in ASCII + if (!mb_detect_encoding($ns, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($ns, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Host conversion to Punycode failed'])); + return; + } else { + $ns = $convertedDomain; + } + } + + // Check for prohibited patterns in nameserver + if (!preg_match('/^((xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){2,}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $ns)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Nameserver invalid format'])); + return; + } + + // Extract TLD from the domain + $parts = explode('.', $ns); + $tld = "." . end($parts); + + $stmt1 = $pdo->prepare("SELECT id, name, clid FROM host WHERE name = :ns"); + $stmt1->bindParam(':ns', $ns, PDO::PARAM_STR); + $stmt1->execute(); + $hostDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + $hostS = true; + $ipS = false; + break; + case 'ip': + // Search by IP + + // Empty IP check + if (!$ns) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter an IP address'])); + return; + } + + // Validate IP address format + if (!filter_var($ns, FILTER_VALIDATE_IP)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Invalid IP address format'])); + return; + } + + $tld = ""; + + $stmt1 = $pdo->prepare(" + SELECT h.id, h.name, h.clid + FROM host h + INNER JOIN host_addr ha ON h.id = ha.host_id + WHERE ha.addr = :ip + "); + $stmt1->bindParam(':ip', $ns, PDO::PARAM_STR); + $stmt1->execute(); + $hostDetails = $stmt1->fetchAll(PDO::FETCH_ASSOC); + $ipS = true; + $hostS = false; + break; + } + + // Check if the nameserver exists + if (!$hostDetails) { + // Nameserver not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested nameserver was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + if ($ipS) { + $rdapResult = []; + foreach ($hostDetails as $individualHostDetail) { + // Query 2: Get status details + $stmt2 = $pdo->prepare("SELECT status FROM host_status WHERE host_id = :host_id"); + $stmt2->bindParam(':host_id', $individualHostDetail['id'], PDO::PARAM_INT); + $stmt2->execute(); + $statuses = $stmt2->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 2a: Get associated status details + $stmt2a = $pdo->prepare("SELECT domain_id FROM domain_host_map WHERE host_id = :host_id"); + $stmt2a->bindParam(':host_id', $individualHostDetail['id'], PDO::PARAM_INT); + $stmt2a->execute(); + $associated = $stmt2a->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 3: Get IP details + $stmt3 = $pdo->prepare("SELECT addr,ip FROM host_addr WHERE host_id = :host_id"); + $stmt3->bindParam(':host_id', $individualHostDetail['id'], PDO::PARAM_INT); + $stmt3->execute(); + $ipDetails = $stmt3->fetchAll(PDO::FETCH_COLUMN, 0); + + // Define the basic events + $events = [ + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + // Build the 'ipAddresses' structure + $ipAddresses = array_reduce($ipDetails, function ($carry, $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $carry['v4'][] = $ip; + } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $carry['v6'][] = $ip; + } + return $carry; + }, ['v4' => [], 'v6' => []]); // Initialize with 'v4' and 'v6' keys + + // Check if both v4 and v6 are empty, then set to empty object for JSON encoding + if (empty($ipAddresses['v4']) && empty($ipAddresses['v6'])) { + $ipAddresses = new stdClass(); // This will encode to {} in JSON + } + + // If there are associated domains, add 'associated' to the statuses + if (!empty($associated)) { + $statuses[] = 'associated'; + } + + // Build the RDAP response for the current host + $rdapResult[] = [ + 'objectClassName' => 'nameserver', + 'handle' => 'H' . $individualHostDetail['id'] . '-' . $c['roid'], + 'ipAddresses' => $ipAddresses, + 'events' => $events, + 'ldhName' => $individualHostDetail['name'], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/nameserver/' . $individualHostDetail['name'], + 'rel' => 'self', + 'type' => 'application/rdap+json', + ] + ], + 'status' => $statuses, + 'remarks' => [ + [ + 'description' => ['This record contains only a summary. For detailed information, please submit a query specifically for this object.'], + 'title' => 'Incomplete Data', + 'type' => 'object truncated due to authorization' + ] + ], + ]; + } + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'nameserverSearchResults' => $rdapResult, + "notices" => [ + [ + "description" => [ + "Access to RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + } elseif ($hostS) { + // Query 2: Get status details + $stmt2 = $pdo->prepare("SELECT status FROM host_status WHERE host_id = :host_id"); + $stmt2->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $statuses = $stmt2->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 2a: Get associated status details + $stmt2a = $pdo->prepare("SELECT domain_id FROM domain_host_map WHERE host_id = :host_id"); + $stmt2a->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt2a->execute(); + $associated = $stmt2a->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 3: Get IP details + $stmt3 = $pdo->prepare("SELECT addr,ip FROM host_addr WHERE host_id = :host_id"); + $stmt3->bindParam(':host_id', $hostDetails['id'], PDO::PARAM_INT); + $stmt3->execute(); + $ipDetails = $stmt3->fetchAll(PDO::FETCH_COLUMN, 0); + + // Query 4: Get registrar details + $stmt4 = $pdo->prepare("SELECT id,name,iana_id,whois_server,rdap_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"); + $stmt4->bindParam(':clid', $hostDetails['clid'], PDO::PARAM_INT); + $stmt4->execute(); + $registrarDetails = $stmt4->fetch(PDO::FETCH_ASSOC); + + // Query 5: Get registrar abuse details + $stmt5 = $pdo->prepare("SELECT first_name,last_name FROM registrar_contact WHERE registrar_id = :clid AND type = 'abuse'"); + $stmt5->bindParam(':clid', $hostDetails['clid'], PDO::PARAM_INT); + $stmt5->execute(); + $registrarAbuseDetails = $stmt5->fetch(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + $abuseContactName = ($registrarAbuseDetails) ? $registrarAbuseDetails['first_name'] . ' ' . $registrarAbuseDetails['last_name'] : ''; + + // Build the 'ipAddresses' structure + $ipAddresses = array_reduce($ipDetails, function ($carry, $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + $carry['v4'][] = $ip; + } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $carry['v6'][] = $ip; + } + return $carry; + }, ['v4' => [], 'v6' => []]); // Initialize with 'v4' and 'v6' keys + + // Check if both v4 and v6 are empty, then set to empty object for JSON encoding + if (empty($ipAddresses['v4']) && empty($ipAddresses['v6'])) { + $ipAddresses = new stdClass(); // This will encode to {} in JSON + } + + // If there are associated domains, add 'associated' to the statuses + if (!empty($associated)) { + $statuses[] = 'associated'; + } + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'nameserverSearchResults' => [ + [ + 'objectClassName' => 'nameserver', + 'entities' => array_merge( + [ + [ + 'objectClassName' => 'entity', + 'entities' => [ + [ + 'objectClassName' => 'entity', + 'roles' => ["abuse"], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $abuseContactName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $registrarDetails['abuse_phone']], + ["email", new stdClass(), "text", $registrarDetails['abuse_email']] + ] + ], + ], + ], + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + "links" => [ + [ + "href" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "value" => $c['rdap_url'] . "/entity/" . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + "rel" => "self", + "type" => "application/rdap+json" + ] + ], + "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], + "remarks" => [ + [ + "description" => ["This record contains only a summary. For detailed information, please submit a query specifically for this object."], + "title" => "Incomplete Data", + "type" => "object truncated" + ] + ], + "roles" => ["registrar"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $registrarDetails['name']] + ] + ], + ], + ], + ), + 'handle' => 'H' . $hostDetails['id'] . '-' . $c['roid'] . '', + 'ipAddresses' => $ipAddresses, + 'events' => $events, + 'ldhName' => $hostDetails['name'], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/nameserver/' . $hostDetails['name'], + 'rel' => 'self', + 'type' => 'application/rdap+json', + ] + ], + 'status' => $statuses, + ], + ], + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + } + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleEntitySearchQuery($request, $response, $pdo, $searchPattern, $c, $log, $searchType) { + // Extract and validate the entity handle from the request + $entity = trim($searchPattern); + + // Empty entity check + if (!$entity) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Please enter an entity'])); + return; + } + + // Check for prohibited patterns in RDAP entity handle + if (!preg_match("/^[A-Za-z0-9]+$/", $entity)) { + $response->header('Content-Type', 'application/json'); + $response->status(400); // Bad Request + $response->end(json_encode(['error' => 'Entity handle invalid format'])); + return; + } + + // Perform the RDAP lookup + try { + switch ($searchType) { + case 'fn': + // Invalidate the search when searching by first name + $entity = null; // Setting to null or an unlikely value to match + break; + case 'handle': + // Handle search by handle + // Assuming $entity is set somewhere above + break; + } + + // Validate $entity to ensure it is numeric and contains only digits + if (!is_numeric($entity)) { + // Return a 404 response if $entity is not a purely numeric string + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested entity was not found in the RDAP database.', + ])); + // Close the connection + $pdo = null; + return; + } + + // Query 1: Get registrar details + $stmt1 = $pdo->prepare("SELECT id,name,clid,iana_id,whois_server,rdap_server,url,email,abuse_email,abuse_phone FROM registrar WHERE iana_id = :iana_id"); + $stmt1->bindParam(':iana_id', $entity, PDO::PARAM_INT); + $stmt1->execute(); + $registrarDetails = $stmt1->fetch(PDO::FETCH_ASSOC); + + // Check if the first query returned a result + if (!$registrarDetails) { + // Query 2: Get registrar details by id as a fallback for ccTLDs without iana_id + $stmt2 = $pdo->prepare("SELECT id, name, clid, iana_id, whois_server, rdap_server, url, email, abuse_email, abuse_phone FROM registrar WHERE id = :id"); + $stmt2->bindParam(':id', $entity, PDO::PARAM_INT); + $stmt2->execute(); + $registrarDetails = $stmt2->fetch(PDO::FETCH_ASSOC); + } + + // Check if the entity exists + if (!$registrarDetails) { + // Entity not found, respond with a 404 error + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/json'); + $response->status(404); + $response->end(json_encode([ + 'errorCode' => 404, + 'title' => 'Not Found', + 'description' => 'The requested entity was not found in the RDAP database.', + "notices" => [ + [ + "description" => [ + "Access to RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ], JSON_UNESCAPED_SLASHES)); + // Close the connection + $pdo = null; + return; + } + + // Query 2: Fetch all contact types for a registrar + $stmt2 = $pdo->prepare("SELECT type, first_name, last_name, voice, email FROM registrar_contact WHERE registrar_id = :clid"); + $stmt2->bindParam(':clid', $registrarDetails['id'], PDO::PARAM_INT); + $stmt2->execute(); + $contacts = $stmt2->fetchAll(PDO::FETCH_ASSOC); + + // Query 3: Get registrar abuse details + $stmt3 = $pdo->prepare("SELECT org,street1,street2,city,sp,pc,cc FROM registrar_contact WHERE registrar_id = :clid AND type = 'owner'"); + $stmt3->bindParam(':clid', $registrarDetails['id'], PDO::PARAM_INT); + $stmt3->execute(); + $registrarContact = $stmt3->fetch(PDO::FETCH_ASSOC); + + // Define the basic events + $events = [ + ['eventAction' => 'last update of RDAP database', 'eventDate' => (new DateTime())->format('Y-m-d\TH:i:s.v\Z')], + ]; + + // Initialize an array to hold entity blocks + $entityBlocks = []; + // Define an array of allowed contact types + $allowedTypes = ['owner', 'tech', 'abuse']; + + foreach ($contacts as $contact) { + // Check if the contact type is one of the allowed types + if (in_array($contact['type'], $allowedTypes)) { + // Build the full name + $fullName = $contact['first_name'] . ' ' . $contact['last_name']; + + // Create an entity block for each allowed contact type + $entityBlock = [ + 'objectClassName' => 'entity', + 'roles' => [$contact['type']], + "status" => ["active"], + "vcardArray" => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), "text", $fullName], + ["tel", ["type" => ["voice"]], "uri", "tel:" . $contact['voice']], + ["email", new stdClass(), "text", $contact['email']] + ] + ], + ]; + + // Add the entity block to the array + $entityBlocks[] = $entityBlock; + } + } + + // Construct the RDAP response in JSON format + $rdapResponse = [ + 'rdapConformance' => [ + 'rdap_level_0', + 'icann_rdap_response_profile_1', + 'icann_rdap_technical_implementation_guide_1', + ], + 'entitySearchResults' => [ + [ + 'objectClassName' => 'entity', + 'entities' => $entityBlocks, + "handle" => (string)($registrarDetails['iana_id'] ?: 'R' . $registrarDetails['id'] . '-' . $c['roid'] . ''), + 'events' => $events, + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/entity/' . ($registrarDetails['iana_id'] ?: $registrarDetails['id']), + 'rel' => 'self', + 'type' => 'application/rdap+json', + ] + ], + "publicIds" => [ + [ + "identifier" => (string)$registrarDetails['iana_id'], + "type" => "IANA Registrar ID" + ] + ], + "roles" => ["registrar"], + "status" => ["active"], + 'vcardArray' => [ + "vcard", + [ + ['version', new stdClass(), 'text', '4.0'], + ["fn", new stdClass(), 'text', $registrarContact['org']], + ["adr", [ + "", // Post office box + $registrarContact['street1'], // Extended address + $registrarContact['street2'], // Street address + $registrarContact['city'], // Locality + $registrarContact['sp'], // Region + $registrarContact['pc'], // Postal code + $registrarContact['cc'] // Country name + ]], + ["email", $registrarDetails['email']], + ] + ], + ], + ], + "notices" => [ + [ + "description" => [ + "Access to " . strtoupper($tld) . " RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ], + [ + "description" => [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + ], + [ + "description" => [ + "For more information on domain status codes, please visit https://icann.org/epp" + ], + "links" => [ + [ + "href" => "https://icann.org/epp", + "value" => "https://icann.org/epp", + "rel" => "glossary", + "type" => "text/html" + ] + ], + "title" => "Status Codes" + ], + [ + "description" => [ + "URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf" + ], + "links" => [ + [ + "href" => "https://icann.org/wicf", + "value" => "https://icann.org/wicf", + "rel" => "help", + "type" => "text/html" + ] + ], + "title" => "RDDS Inaccuracy Complaint Form" + ], + ] + ]; + + // Send the RDAP response + try { + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'web-whois-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['Database error:' => $e->getMessage()])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); + } catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + $response->header('Content-Type', 'application/json'); + $response->status(503); + $response->end(json_encode(['error' => 'Error connecting to the RDAP database'])); + return; + } catch (Throwable $e) { + $log->error('Error: ' . $e->getMessage()); + $response->status(500); + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['General error:' => $e->getMessage()])); + return; + } +} + +function handleHelpQuery($request, $response, $pdo, $c) { + // Set the RDAP conformance levels + $rdapConformance = [ + "rdap_level_0", + "icann_rdap_response_profile_1", + "icann_rdap_technical_implementation_guide_1" + ]; + + // Set the descriptions and links for the help section + $helpNotices = [ + "description" => [ + "domain/XXXX", + "nameserver/XXXX", + "entity/XXXX", + "domains?name=XXXX", + "domains?nsLdhName=XXXX", + "domains?nsIp=XXXX", + "nameservers?name=XXXX", + "nameservers?ip=XXXX", + "entities?fn=XXXX", + "entities?handle=XXXX", + "help/XXXX" + ], + 'links' => [ + [ + 'href' => $c['rdap_url'] . '/help', + 'rel' => 'self', + 'type' => 'application/rdap+json', + ], + [ + 'href' => 'https://namingo.org', + 'rel' => 'related', + 'type' => 'application/rdap+json', + ] + ], + "title" => "RDAP Help" + ]; + + // Set the terms of service + $termsOfService = [ + "description" => [ + "Access to RDAP information is provided to assist persons in determining the contents of a domain name registration record in the Domain Name Registry registry database.", + "The data in this record is provided by Domain Name Registry for informational purposes only, and Domain Name Registry does not guarantee its accuracy. ", + "This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to: (a) allow,", + "enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or", + "(b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or NIC except as reasonably necessary to register domain names or modify existing registrations.", + "All rights reserved. Domain Name Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy." + ], + "links" => [ + [ + "href" => $c['rdap_url'] . "/help", + "value" => $c['rdap_url'] . "/help", + "rel" => "self", + "type" => "application/rdap+json" + ], + [ + "href" => $c['registry_url'], + "value" => $c['registry_url'], + "rel" => "alternate", + "type" => "text/html" + ], + ], + "title" => "RDAP Terms of Service" + ]; + + // Construct the RDAP response for help query + $rdapResponse = [ + "rdapConformance" => $rdapConformance, + "notices" => [ + $helpNotices, + $termsOfService + ] + ]; + + // Send the RDAP response + $response->header('Content-Type', 'application/rdap+json'); + $response->status(200); + $response->end(json_encode($rdapResponse, JSON_UNESCAPED_SLASHES)); +} \ No newline at end of file diff --git a/whois/port43/whois_limited.php b/whois/port43/whois_limited.php new file mode 100644 index 0000000..4f7ad10 --- /dev/null +++ b/whois/port43/whois_limited.php @@ -0,0 +1,427 @@ +withDriver($c['db_type']) + ->withHost($c['db_host']) + ->withPort($c['db_port']) + ->withDbName($c['db_database']) + ->withUsername($c['db_username']) + ->withPassword($c['db_password']) + ->withCharset('utf8mb4') +); + +// Create a Swoole TCP server +$server = new Server($c['whois_ipv4'], 43); +if ($c['whois_ipv6'] !== false) { + $server->addListener($c['whois_ipv6'], 43, SWOOLE_SOCK_TCP6); +} +$server->set([ + 'daemonize' => false, + 'log_file' => '/var/log/namingo/whois_application.log', + 'log_level' => SWOOLE_LOG_INFO, + 'worker_num' => swoole_cpu_num() * 2, + 'pid_file' => '/var/run/whois.pid', + 'max_request' => 1000, + 'dispatch_mode' => 2, + 'open_tcp_nodelay' => true, + 'max_conn' => 1024, + 'heartbeat_check_interval' => 60, + 'heartbeat_idle_time' => 120, + 'buffer_output_size' => 2 * 1024 * 1024, // 2MB + 'enable_reuse_port' => true, + 'package_max_length' => 8192, // 8KB + 'open_eof_check' => true, + 'package_eof' => "\r\n" +]); + +$rateLimiter = new Rately(); +$log->info('server started.'); + +// Register a callback to handle incoming connections +$server->on('connect', function ($server, $fd) use ($log) { + $log->info('new client connected: ' . $fd); +}); + +// Register a callback to handle incoming requests +$server->on('receive', function ($server, $fd, $reactorId, $data) use ($c, $pool, $log, $rateLimiter) { + // Get a PDO connection from the pool + try { + $pdo = $pool->get(); + if (!$pdo) { + throw new PDOException("Failed to retrieve a connection from Swoole PDOPool."); + } + } catch (PDOException $e) { + $log->alert("Swoole PDO Pool failed: " . $e->getMessage()); + $server->send($fd, "Database failure. Please try again later"); + $server->close($fd); + return; + } + $privacy = $c['privacy']; + $minimum_data = $c['minimum_data']; + $parsedQuery = parseQuery($data); + $queryType = $parsedQuery['type']; + $queryData = $parsedQuery['data']; + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + + if (!isIpWhitelisted($remoteAddr, $pdo)) { + if (($c['rately'] == true) && ($rateLimiter->isRateLimited('whois', $remoteAddr, $c['limit'], $c['period']))) { + $log->error('rate limit exceeded for ' . $remoteAddr); + $server->send($fd, "rate limit exceeded. Please try again later"); + $server->close($fd); + return; + } + } + + // Handle the WHOIS query + try { + switch ($queryType) { + case 'domain': + // Handle domain query + $domain = $queryData; + + if (!$domain) { + $server->send($fd, "please enter a domain name"); + $server->close($fd); + return; + } + if (strlen($domain) > 68) { + $server->send($fd, "domain name is too long"); + $server->close($fd); + return; + } + // Convert to Punycode if the domain is not in ASCII + if (!mb_detect_encoding($domain, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $server->send($fd, "Domain conversion to Punycode failed"); + $server->close($fd); + return; + } else { + $domain = $convertedDomain; + } + } + if (!preg_match('/^(?:(xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){1,3}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $domain)) { + $server->send($fd, "domain name invalid format"); + $server->close($fd); + return; + } + $domain = strtoupper($domain); + + // Extract TLD from the domain and prepend a dot + $parts = explode('.', $domain); + + // Handle multi-segment TLDs (e.g., co.uk, ngo.us, etc.) + if (count($parts) > 2) { + $tld = "." . $parts[count($parts) - 2] . "." . $parts[count($parts) - 1]; + } else { + $tld = "." . end($parts); + } + + // Check if the TLD exists in the domain_tld table + $stmtTLD = $pdo->prepare("SELECT COUNT(*) FROM domain_tld WHERE tld = :tld"); + $stmtTLD->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtTLD->execute(); + $tldExists = $stmtTLD->fetchColumn(); + + if (!$tldExists) { + $server->send($fd, "Invalid TLD. Please search only allowed TLDs"); + $server->close($fd); + return; + } + + // Fetch the IDN regex for the given TLD + $stmtRegex = $pdo->prepare("SELECT idn_table FROM domain_tld WHERE tld = :tld"); + $stmtRegex->bindParam(':tld', $tld, PDO::PARAM_STR); + $stmtRegex->execute(); + $idnRegex = $stmtRegex->fetchColumn(); + + if (!$idnRegex) { + $server->send($fd, "Failed to fetch domain IDN table"); + $server->close($fd); + return; + } + + // Check for invalid characters using fetched regex + if (strpos(strtolower($parts[0]), 'xn--') === 0) { + $label = idn_to_utf8(strtolower($parts[0]), IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $label = strtolower($parts[0]); + } + if (!preg_match($idnRegex, $label)) { + $server->send($fd, "Domain name invalid IDN characters"); + $server->close($fd); + return; + } + + $query = "SELECT * FROM registry.domain WHERE name = :domain"; + $stmt = $pdo->prepare($query); + $stmt->bindParam(':domain', $domain, PDO::PARAM_STR); + $stmt->execute(); + + if ($f = $stmt->fetch(PDO::FETCH_ASSOC)) { + $f['crdate'] = (new DateTime($f['crdate']))->format('Y-m-d\TH:i:s.v\Z'); + if (isset($f['lastupdate']) && $f['lastupdate'] !== null) { + $f['lastupdate'] = (new DateTime($f['lastupdate']))->format('Y-m-d\TH:i:s.v\Z'); + } else { + $f['lastupdate'] = ''; + } + $f['exdate'] = (new DateTime($f['exdate']))->format('Y-m-d\TH:i:s.v\Z'); + + $query2 = "SELECT tld FROM domain_tld WHERE id = :tldid"; + $stmt2 = $pdo->prepare($query2); + $stmt2->bindParam(':tldid', $f['tldid'], PDO::PARAM_INT); + $stmt2->execute(); + + $tld = $stmt2->fetch(PDO::FETCH_ASSOC); + + $query3 = "SELECT name,iana_id,whois_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"; + $stmt3 = $pdo->prepare($query3); + $stmt3->bindParam(':clid', $f['clid'], PDO::PARAM_INT); + $stmt3->execute(); + + $clidF = $stmt3->fetch(PDO::FETCH_ASSOC); + + // Check if the domain name is non-ASCII or starts with 'xn--' + $isNonAsciiOrPunycode = !mb_check_encoding($f['name'], 'ASCII') || strpos($f['name'], 'xn--') === 0; + + $res = "Domain Name: ".strtoupper($f['name']) + ."\n"; + + // Add the Internationalized Domain Name line if the condition is met + if ($isNonAsciiOrPunycode) { + // Convert the domain name to UTF-8 and make it uppercase + $internationalizedName = idn_to_utf8($f['name'], 0, INTL_IDNA_VARIANT_UTS46); + $res .= "Internationalized Domain Name: " . mb_strtoupper($internationalizedName) . "\n"; + } + + $res .= "Registrar: ".$clidF['name']; + + $query9 = "SELECT h.name, + GROUP_CONCAT(ha.addr ORDER BY INET6_ATON(ha.addr) SEPARATOR ', ') AS ips + FROM domain_host_map dhm + JOIN host h ON dhm.host_id = h.id + LEFT JOIN host_addr ha ON h.id = ha.host_id + WHERE dhm.domain_id = :domain_id + GROUP BY h.name"; + $stmt9 = $pdo->prepare($query9); + $stmt9->bindParam(':domain_id', $f['id'], PDO::PARAM_INT); + $stmt9->execute(); + + $counter = 0; + while ($counter < 13) { + $f2 = $stmt9->fetch(PDO::FETCH_ASSOC); + if ($f2 === false) break; // Break if there are no more rows + $res .= "\nName Server: ".$f2['name']; + if (!empty($f2['ips'])) { + $res .= " (".$f2['ips'].")"; // Append IPs if available + } + $counter++; + } + + $currentDateTime = new DateTime(); + $currentTimestamp = $currentDateTime->format("Y-m-d\TH:i:s.v\Z"); + $res .= "\n"; + $server->send($fd, $res . ""); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $domain . ' | FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } else { + // Check if domain is reserved + $stmtReserved = $pdo->prepare("SELECT id FROM reserved_domain_names WHERE name = ? LIMIT 1"); + $stmtReserved->execute([$parts[0]]); + $domain_already_reserved = $stmtReserved->fetchColumn(); + + if ($domain_already_reserved) { + $server->send($fd, "Domain name is reserved or restricted"); + $server->close($fd); + return; + } + + //NOT FOUND or No match for; + $server->send($fd, "NOT FOUND"); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $domain . ' | NOT FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } + break; + case 'nameserver': + // Handle nameserver query + $nameserver = $queryData; + + if (!$nameserver) { + $server->send($fd, "please enter a nameserver"); + $server->close($fd); + } + if (strlen($nameserver) > 63) { + $server->send($fd, "nameserver is too long"); + $server->close($fd); + } + + // Convert to Punycode if the host is not in ASCII + if (!mb_detect_encoding($nameserver, 'ASCII', true)) { + $convertedDomain = idn_to_ascii($nameserver, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($convertedDomain === false) { + $server->send($fd, "Host conversion to Punycode failed."); + $server->close($fd); + } else { + $nameserver = $convertedDomain; + } + } + + if (!preg_match('/^((xn--[a-zA-Z0-9-]{1,63}|[a-zA-Z0-9-]{1,63})\.){2,}(xn--[a-zA-Z0-9-]{2,63}|[a-zA-Z]{2,63})$/', $nameserver)) { + $server->send($fd, "Nameserver contains invalid characters or is not in the correct format."); + $server->close($fd); + } + + $query = "SELECT name,clid FROM host WHERE name = :nameserver"; + $stmt = $pdo->prepare($query); + $stmt->bindParam(':nameserver', $nameserver, PDO::PARAM_STR); + $stmt->execute(); + + if ($f = $stmt->fetch(PDO::FETCH_ASSOC)) { + $res = "Server Name: ".$f['name']; + + // Fetch the registrar details for this registrar using the id + $regQuery = "SELECT id,name,iana_id,whois_server,url,abuse_email,abuse_phone FROM registrar WHERE id = :clid"; + $regStmt = $pdo->prepare($regQuery); + $regStmt->bindParam(':clid', $f['clid'], PDO::PARAM_INT); + $regStmt->execute(); + + if ($registrar = $regStmt->fetch(PDO::FETCH_ASSOC)) { + // Append the registrar details to the response + $res .= "\nRegistrar Name: ".$registrar['name']; + } + + $res .= "\n"; + $server->send($fd, $res . ""); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $nameserver . ' | FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } else { + //NOT FOUND or No match for; + $server->send($fd, "NOT FOUND"); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $nameserver . ' | NOT FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } + break; + case 'registrar': + // Handle registrar query + $registrar = $queryData; + + if (!$registrar) { + $server->send($fd, "please enter a registrar name"); + $server->close($fd); + } + if (strlen($registrar) > 50) { + $server->send($fd, "registrar name is too long"); + $server->close($fd); + } + + if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $registrar)) { + $server->send($fd, "Registrar name contains invalid characters."); + $server->close($fd); + } + + $query = "SELECT id,name,iana_id,whois_server,url,abuse_email,abuse_phone FROM registrar WHERE name = :registrar"; + $stmt = $pdo->prepare($query); + $stmt->bindParam(':registrar', $registrar, PDO::PARAM_STR); + $stmt->execute(); + + if ($f = $stmt->fetch(PDO::FETCH_ASSOC)) { + $res = "Registrar: ".$f['name']; + + $res .= "\n"; + $server->send($fd, $res . ""); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $registrar . ' | FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } else { + //NOT FOUND or No match for; + $server->send($fd, "NOT FOUND"); + + $clientInfo = $server->getClientInfo($fd); + $remoteAddr = $clientInfo['remote_ip']; + $log->notice('new request from ' . $remoteAddr . ' | ' . $registrar . ' | NOT FOUND'); + + $stmt = $pdo->prepare("UPDATE settings SET value = value + 1 WHERE name = :name"); + $settingName = 'whois-43-queries'; + $stmt->bindParam(':name', $settingName); + $stmt->execute(); + } + break; + default: + // Handle unknown query type + $log->error('Error'); + $server->send($fd, "Error"); + } + } catch (PDOException $e) { + // Handle database exceptions + $log->error('Database error: ' . $e->getMessage()); + $server->send($fd, "Error connecting to the whois database"); + $server->close($fd); + } catch (Throwable $e) { + // Catch any other exceptions or errors + $log->error('Error: ' . $e->getMessage()); + $server->send($fd, "Error"); + $server->close($fd); + } finally { + // Return the connection to the pool + $pool->put($pdo); + $server->close($fd); + } +}); + +// Register a callback to handle client disconnections +$server->on('close', function ($server, $fd) use ($log) { + $log->info('client ' . $fd . ' disconnected.'); +}); + +// Start the server +$server->start(); \ No newline at end of file