From 8428a05bfb666b6389be1310651b1d8b0a5bf2e7 Mon Sep 17 00:00:00 2001 From: Pinga <121483313+getpinga@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:34:52 +0200 Subject: [PATCH] EPP domain check logic and speed improvements Also better domain validation as a whole --- epp/src/epp-check.php | 79 +++++++++++++++++------------------ epp/src/helpers.php | 96 +++++++++++++++++++++---------------------- 2 files changed, 86 insertions(+), 89 deletions(-) diff --git a/epp/src/epp-check.php b/epp/src/epp-check.php index bf760e4..5c1fff2 100644 --- a/epp/src/epp-check.php +++ b/epp/src/epp-check.php @@ -255,62 +255,59 @@ function processDomainCheck($conn, $db, $xml, $trans, $clid) { $names = []; foreach ($domains as $domain) { $domainName = (string) $domain; - - // Check if the domain is already taken - $stmt = $db->prepare("SELECT name FROM domain WHERE name = :domainName"); - $stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR); - $stmt->execute(); - $taken = $stmt->fetchColumn(); - $availability = $taken ? '0' : '1'; - - // Initialize a new domain entry with the domain name $domainEntry = [$domainName]; - - if ($availability === '0') { - // Domain is taken - $domainEntry[] = 0; // Set status to unavailable - $domainEntry[] = 'In use'; - } else { - // Check if the domain is reserved - $parts = extractDomainAndTLD($domainName); - $label = $parts['domain']; - $stmt = $db->prepare("SELECT type FROM reserved_domain_names WHERE name = :domainName LIMIT 1"); - $stmt->bindParam(':domainName', $label, PDO::PARAM_STR); - $stmt->execute(); - $reserved = $stmt->fetchColumn(); + $invalid_label = validate_label($domainName, $db); + if ($invalid_label) { + $domainEntry[] = 0; // Unavailable + $domainEntry[] = ucfirst($invalid_label); + $names[] = $domainEntry; + continue; + } - if ($reserved) { + $parts = extractDomainAndTLD($domainName); + $label = $parts['domain']; + + $stmt = $db->prepare(" + SELECT 'taken' AS type FROM domain WHERE name = :domainName + UNION ALL + SELECT type FROM reserved_domain_names WHERE name = :label + LIMIT 1 + "); + $stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR); + $stmt->bindParam(':label', $label, PDO::PARAM_STR); + $stmt->execute(); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result) { + if ($result['type'] === 'taken') { + // Domain is registered + $domainEntry[] = 0; // Unavailable + $domainEntry[] = 'In use'; + } else { + // Domain is reserved if ($allocation_token !== null) { $allocationTokenValue = (string)$allocation_token; - - $stmt = $db->prepare("SELECT token FROM allocation_tokens WHERE domain_name = :domainName AND token = :token LIMIT 1"); - $stmt->bindParam(':domainName', $label, PDO::PARAM_STR); + $stmt = $db->prepare("SELECT token FROM allocation_tokens WHERE domain_name = :label AND token = :token LIMIT 1"); + $stmt->bindParam(':label', $label, PDO::PARAM_STR); $stmt->bindParam(':token', $allocationTokenValue, PDO::PARAM_STR); $stmt->execute(); $token = $stmt->fetchColumn(); - + if ($token) { - $domainEntry[] = 1; + $domainEntry[] = 1; // Available with a valid allocation token } else { $domainEntry[] = 0; $domainEntry[] = 'Allocation Token mismatch'; } } else { - $domainEntry[] = 0; // Set status to unavailable - $domainEntry[] = ucfirst($reserved); // Capitalize the first letter - } - } else { - $invalid_label = validate_label($domainName, $db); - - // Check if the domain is Invalid - if ($invalid_label) { - $domainEntry[] = 0; // Set status to unavailable - $domainEntry[] = ucfirst($invalid_label); // Capitalize the first letter - } else { - $domainEntry[] = 1; // Domain is available + $domainEntry[] = 0; // Unavailable + $domainEntry[] = ucfirst($result['type']); // Reserved reason } } + } else { + // Domain is available + $domainEntry[] = 1; } // Append this domain entry to names @@ -433,7 +430,7 @@ function processDomainCheck($conn, $db, $xml, $trans, $clid) { 'clTRID' => $clTRID, 'svTRID' => $svTRID, ]; - if ($fees) { + if (!empty($fees)) { $response['fees'] = $fees; } } diff --git a/epp/src/helpers.php b/epp/src/helpers.php index 090f9d6..6374d81 100644 --- a/epp/src/helpers.php +++ b/epp/src/helpers.php @@ -182,67 +182,67 @@ function validate_identifier($identifier) { } } -function isDomainValid(string $domain): bool { - // Ensure the domain only contains valid characters (letters, numbers, hyphens, and dots) - if (!preg_match('/^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/', $domain)) { - return false; - } - - // Split the domain into its labels (subdomains, SLD, etc.) - $labels = explode('.', $domain); - foreach ($labels as $label) { - if (strlen($label) > 63 || empty($label)) { // Labels cannot be empty or exceed 63 chars - return false; - } - } - return true; -} - -function validate_label($label, $pdo) { - if (!$label) { +function validate_label($domain, $pdo) { + if (!$domain) { return 'You must enter a domain name'; } - // Ensure input contains only valid domain characters - if (!preg_match('/^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/', $label)) { - return 'Invalid domain name format: only letters, numbers, dots, and hyphens allowed'; + // Ensure domain has at least one dot (.) separating labels + if (strpos($domain, '.') === false) { + return 'Invalid domain name format: must contain at least one dot (.)'; } - if (!isDomainValid($label)) { - return 'Domain label is too long (exceeds 63 characters)'; - } + // Split domain into labels (subdomains, SLD, TLD) + $labels = explode('.', $domain); + foreach ($labels as $label) { + $len = strlen($label); + + // Labels cannot be empty, shorter than 2, or longer than 63 characters + if ($len < 2 || $len > 63) { + return 'Each domain label must be between 2 and 63 characters'; + } - // Ensure domain starts and ends with an alphanumeric character - $parts = explode('.', $label); - foreach ($parts as $part) { - if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/', $part)) { + if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/', $label)) { return 'Each domain label must start and end with an alphanumeric character'; } + + // Check if it's a Punycode label (IDN) + if (strpos($label, 'xn--') === 0) { + // Ensure valid Punycode structure + if (!preg_match('/^xn--[a-zA-Z0-9-]+$/', $label)) { + return 'Invalid Punycode format'; + } + + // Convert Punycode to UTF-8 + $decoded = idn_to_utf8($label, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + if ($decoded === false || $decoded === '') { + return 'Invalid Punycode conversion'; + } + + // Ensure decoded label follows normal domain rules + if (!preg_match('/^[\p{L}0-9][\p{L}0-9-]*[\p{L}0-9]$/u', $decoded)) { + return 'IDN must start and end with a letter or number'; + } + } else { + // Prevent consecutive or invalid hyphen usage + if (preg_match('/--|\.\./', $label)) { + return 'Domain labels cannot contain consecutive dashes (--) or dots (..)'; + } + } } // Extract domain and TLD - $parts = extractDomainAndTLD($label); + $parts = extractDomainAndTLD($domain); if (!$parts || empty($parts['domain']) || empty($parts['tld'])) { return 'Invalid domain structure, unable to parse domain name'; } $tld = "." . $parts['tld']; - if (strlen($parts['domain']) > 63) { - return 'Total length of your domain must be less than 63 characters'; - } - - if (strlen($parts['domain']) < 2) { - return 'Total length of your domain must be greater than 2 characters'; - } - - if (strpos($label, '.') === false) { - return 'Invalid domain name format, must contain at least one dot (.)'; - } - - // Ensure no invalid use of hyphens (double dashes, leading/trailing dashes) - if (strpos($parts['domain'], 'xn--') === false && preg_match("/(^-|^\.|-\.|\.-|--|\.\.|-$|\.$)/", $parts['domain'])) { - return 'Domain name cannot contain consecutive dashes (--) unless it is a punycode domain'; + // Validate domain length + $domainLength = strlen($parts['domain']); + if ($domainLength < 2 || $domainLength > 63) { + return 'Domain length must be between 2 and 63 characters'; } // Check if the TLD exists in the domain_tld table @@ -255,11 +255,11 @@ function validate_label($label, $pdo) { return 'Zone is not supported'; } - // If domain is an IDN (Punycode), convert it to Unicode + // IDN-specific validation (only if the domain contains Punycode) if (strpos($parts['domain'], 'xn--') === 0) { $label = idn_to_utf8($parts['domain'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); - // Fetch the IDN regex for the given TLD (only if it's an IDN) + // 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(); @@ -269,8 +269,8 @@ function validate_label($label, $pdo) { return 'Failed to fetch domain IDN table'; } - // Check for invalid characters using fetched regex - if (!preg_match($idnRegex['idn_table'], $label)) { + // Check against IDN regex + if (!preg_match($idnRegex, $label)) { return 'Invalid domain name format, please review registry policy about accepted labels'; } }