<?php
/**
 * DNS Validator - Robust DNS validation for WHP
 * 
 * This class handles DNS validation for SSL certificate requests,
 * including dynamic server IP detection and fallback DNS servers.
 * 
 * @version 1.0.0
 * @author WHP Development Team
 */

class dns_validator {
    
    private $server_ip;
    private $fallback_dns_servers;
    
    public function __construct() {
        $this->fallback_dns_servers = [
            '8.8.8.8',    // Google DNS
            '1.1.1.1',    // Cloudflare DNS
            '9.9.9.9'     // Quad9 DNS
        ];
        
        $this->server_ip = $this->getServerIP();
    }
    
    /**
     * Get the server's IP address using multiple detection methods
     * 
     * @return string Server IP address
     */
    private function getServerIP() {
        // Method 1: Try to get from environment variables
        $server_ip = $_SERVER['SERVER_ADDR'] ?? null;
        
        // Method 2: Try to get from network interface
        if (empty($server_ip) || $server_ip === '127.0.0.1' || $server_ip === '::1') {
            $output = shell_exec("ip route get 8.8.8.8 | awk '{print \$7}' | head -1 2>/dev/null");
            if ($output) {
                $server_ip = trim($output);
            }
        }
        
        // Method 3: Try hostname -I
        if (empty($server_ip) || $server_ip === '127.0.0.1') {
            $output = shell_exec("hostname -I | awk '{print \$1}' 2>/dev/null");
            if ($output) {
                $server_ip = trim($output);
            }
        }
        
        // Method 4: Try external IP detection (for production servers)
        if (empty($server_ip) || $this->isPrivateIP($server_ip)) {
            $external_ip = $this->getExternalIP();
            if ($external_ip) {
                $server_ip = $external_ip;
            }
        }
        
        // Fallback to localhost if all methods fail
        if (empty($server_ip)) {
            $server_ip = '127.0.0.1';
        }
        
        return $server_ip;
    }
    
    /**
     * Check if an IP address is private/internal
     * 
     * @param string $ip IP address to check
     * @return bool True if private IP
     */
    private function isPrivateIP($ip) {
        $private_ranges = [
            '10.0.0.0/8',
            '172.16.0.0/12', 
            '192.168.0.0/16',
            '127.0.0.0/8'
        ];
        
        foreach ($private_ranges as $range) {
            if ($this->ipInRange($ip, $range)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if IP is in CIDR range
     * 
     * @param string $ip IP address
     * @param string $range CIDR range
     * @return bool True if IP is in range
     */
    private function ipInRange($ip, $range) {
        list($subnet, $mask) = explode('/', $range);
        $ip_binary = ip2long($ip);
        $subnet_binary = ip2long($subnet);
        $mask_binary = ~((1 << (32 - $mask)) - 1);
        
        return ($ip_binary & $mask_binary) === ($subnet_binary & $mask_binary);
    }
    
    /**
     * Get external IP address
     * 
     * @return string|null External IP or null if failed
     */
    private function getExternalIP() {
        $services = [
            'https://ifconfig.me',
            'https://icanhazip.com',
            'https://ipinfo.io/ip'
        ];
        
        foreach ($services as $service) {
            $context = stream_context_create([
                'http' => [
                    'timeout' => 5,
                    'user_agent' => 'WHP-DNS-Validator/1.0'
                ]
            ]);
            
            $ip = @file_get_contents($service, false, $context);
            if ($ip && filter_var(trim($ip), FILTER_VALIDATE_IP)) {
                return trim($ip);
            }
        }
        
        return null;
    }
    
    /**
     * Perform DNS lookup with fallback servers
     * 
     * @param string $domain Domain to lookup
     * @param int $type DNS record type (DNS_A, DNS_AAAA, etc.)
     * @return array DNS records or empty array if failed
     */
    public function dnsLookup($domain, $type = DNS_A) {
        // Try system DNS first
        $records = dns_get_record($domain, $type);
        if (!empty($records)) {
            return $records;
        }
        
        // Try fallback DNS servers
        foreach ($this->fallback_dns_servers as $dns_server) {
            $records = $this->dnsLookupWithServer($domain, $type, $dns_server);
            if (!empty($records)) {
                return $records;
            }
        }
        
        return [];
    }
    
    /**
     * Perform DNS lookup using specific DNS server
     * 
     * @param string $domain Domain to lookup
     * @param int $type DNS record type
     * @param string $dns_server DNS server to use
     * @return array DNS records or empty array if failed
     */
    private function dnsLookupWithServer($domain, $type, $dns_server) {
        // Use dig command with specific DNS server
        $type_str = $this->getDnsTypeString($type);
        $command = "dig @{$dns_server} {$domain} {$type_str} +short 2>/dev/null";
        $output = shell_exec($command);
        
        if (empty($output)) {
            return [];
        }
        
        $lines = array_filter(array_map('trim', explode("\n", $output)));
        $records = [];
        
        foreach ($lines as $line) {
            if (filter_var($line, FILTER_VALIDATE_IP)) {
                $records[] = ['ip' => $line];
            }
        }
        
        return $records;
    }
    
    /**
     * Convert DNS type constant to string
     * 
     * @param int $type DNS type constant
     * @return string DNS type string
     */
    private function getDnsTypeString($type) {
        switch ($type) {
            case DNS_A:
                return 'A';
            case DNS_AAAA:
                return 'AAAA';
            case DNS_MX:
                return 'MX';
            case DNS_NS:
                return 'NS';
            case DNS_TXT:
                return 'TXT';
            case DNS_CNAME:
                return 'CNAME';
            default:
                return 'A';
        }
    }
    
    /**
     * Validate that domains point to this server
     * 
     * @param array $domains Array of domain names to validate
     * @return array Validation results
     */
    public function validateDomains($domains) {
        $results = [
            'server_ip' => $this->server_ip,
            'all_valid' => true,
            'domains' => []
        ];
        
        foreach ($domains as $domain) {
            $dns_records = $this->dnsLookup($domain, DNS_A);
            $domain_ips = array_column($dns_records, 'ip');
            
            $is_valid = in_array($this->server_ip, $domain_ips);
            
            $results['domains'][$domain] = [
                'valid' => $is_valid,
                'found_ips' => $domain_ips,
                'expected_ip' => $this->server_ip,
                'message' => $is_valid 
                    ? "Domain points to our server ({$this->server_ip})"
                    : "Domain does not point to our server. Found: " . implode(', ', $domain_ips) . ", Expected: {$this->server_ip}"
            ];
            
            if (!$is_valid) {
                $results['all_valid'] = false;
            }
        }
        
        return $results;
    }
    
    /**
     * Get server IP address
     * 
     * @return string Server IP address
     */
    public function getServerIPAddress() {
        return $this->server_ip;
    }
    
    /**
     * Get fallback DNS servers
     * 
     * @return array Array of fallback DNS servers
     */
    public function getFallbackDNSServers() {
        return $this->fallback_dns_servers;
    }
}
?> 