diff --git a/README.md b/README.md index 04e81f1..0e3a461 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,17 @@ A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofee - **Mobile Optimized** - Full mobile Safari support with PWA capabilities - **CSRF Protection** - Secure form submissions - **Developer Tools** - Database backup/restore, schema sync, and error log viewer +- **PTR Record Management** - AWS Route53 integration for checking reverse DNS records ## What's New +### PTR Records Tab +- **AWS Route53 Integration** - Connect to your AWS account to list A records from hosted zones +- **PTR Lookup** - Check if PTR (reverse DNS) records match your forward DNS hostnames +- **Bulk Checking** - Check all PTR records in a zone with one click +- **Status Indicators** - Visual indicators for MATCH, MISMATCH, and MISSING PTR records +- **Zone Selector** - Easily switch between multiple hosted zones + ### Developer Mode Tab - **Database Backup** - Export full database backup as JSON (entries, settings, logos, audit log) - **Database Import** - Restore from a previously exported backup file @@ -189,6 +197,38 @@ When enabled, new IP entries are automatically enriched with: - Timezone and coordinates - Security flags (proxy, VPN, Tor, threat, etc.) +### AWS Route53 Integration + +Configure AWS credentials in the Advanced tab to enable PTR record checking: + +1. Create an IAM user in AWS with `route53:ListHostedZones` and `route53:ListResourceRecordSets` permissions +2. Generate an access key and secret key for the IAM user +3. In the Advanced tab, enter: + - **AWS Access Key ID** - Your IAM access key + - **AWS Secret Access Key** - Your IAM secret key + - **AWS Region** - Select your preferred region (Route53 is global, but a region is required for API signing) + - **Hosted Zone IDs** - Comma-separated list of Route53 hosted zone IDs (e.g., `Z1234567890ABC, Z0987654321DEF`) +4. Click "Test Connection" to verify credentials +5. Go to the PTR tab to view A records and check PTR status + +**IAM Policy Example:** +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZones", + "route53:ListResourceRecordSets", + "route53:GetHostedZone" + ], + "Resource": "*" + } + ] +} +``` + ### Webhook Integration Configure webhooks in the Advanced tab to notify n8n when data changes: @@ -408,6 +448,49 @@ Content-Type: application/json GET api.php?action=system_info ``` +### AWS Settings +``` +GET api.php?action=aws_settings_get +``` +Returns current AWS Route53 configuration. + +``` +POST api.php?action=aws_settings_save +Content-Type: application/json + +{ + "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE", + "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "aws_region": "us-east-1", + "aws_hosted_zones": "Z1234567890ABC, Z0987654321DEF", + "csrf_token": "..." +} +``` + +### Test AWS Connection +``` +GET api.php?action=aws_test +``` +Tests AWS credentials and returns hosted zone count. + +### List AWS Hosted Zones +``` +GET api.php?action=aws_zones +``` +Returns list of configured hosted zones with their names and IDs. + +### List A Records +``` +GET api.php?action=aws_records&zone_id=Z1234567890ABC +``` +Returns all A records from the specified hosted zone. + +### PTR Lookup +``` +GET api.php?action=ptr_lookup&ip=192.168.1.1 +``` +Performs reverse DNS lookup and returns the PTR record for the given IP. + ## Geofeed Format (RFC 8805) Each line in the exported CSV follows this format: @@ -481,6 +564,22 @@ Ensure your IP prefixes are in valid CIDR notation (e.g., `192.168.1.0/24`) - Ensure your browser/OS has dark mode enabled - Try clearing browser cache +### AWS Route53 connection fails +- Verify your AWS Access Key ID and Secret Access Key are correct +- Ensure the IAM user has the required permissions (ListHostedZones, ListResourceRecordSets) +- Check that the hosted zone IDs are correct (format: Z followed by alphanumeric characters) +- Review error logs in the Developer tab for detailed error messages + +### PTR records showing as MISSING +- PTR records are managed by your IP provider (e.g., IPXO, your ISP, or cloud provider) +- Contact your IP provider to set up reverse DNS for your IP addresses +- PTR lookups use the server's DNS resolver - results may vary based on DNS propagation + +### PTR records showing as MISMATCH +- The PTR record exists but doesn't match the expected hostname +- Verify the PTR is set correctly with your IP provider +- Note: Trailing dots in hostnames are normalized during comparison + ## License MIT License - Feel free to use and modify as needed. diff --git a/database/schema.sql b/database/schema.sql index 60f3a49..76e6f5b 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -126,7 +126,11 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES ('n8n_webhook_enabled', '0'), ('n8n_webhook_delay_minutes', '3'), ('ipregistry_api_key', ''), -('ipregistry_enabled', '0') +('ipregistry_enabled', '0'), +('aws_access_key_id', ''), +('aws_secret_access_key', ''), +('aws_region', 'us-east-1'), +('aws_hosted_zones', '') ON DUPLICATE KEY UPDATE setting_key = setting_key; -- ============================================ diff --git a/webapp/api.php b/webapp/api.php index 40405ff..a3f861a 100644 --- a/webapp/api.php +++ b/webapp/api.php @@ -170,6 +170,30 @@ try { handleErrorLogsClear($db); break; + case 'aws_settings_get': + handleAwsSettingsGet($db); + break; + + case 'aws_settings_save': + handleAwsSettingsSave($db); + break; + + case 'aws_test': + handleAwsTest($db); + break; + + case 'aws_zones': + handleAwsZones($db); + break; + + case 'aws_records': + handleAwsRecords($db); + break; + + case 'ptr_lookup': + handlePtrLookup($db); + break; + default: jsonResponse(['error' => 'Invalid action'], 400); } @@ -2171,3 +2195,372 @@ function formatBytes($bytes, $precision = 2) { $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } + +/** + * Get AWS settings + */ +function handleAwsSettingsGet($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + jsonResponse([ + 'success' => true, + 'data' => [ + 'aws_access_key_id' => getSetting($db, 'aws_access_key_id', ''), + 'aws_secret_access_key' => getSetting($db, 'aws_secret_access_key', '') ? '••••••••' : '', + 'aws_region' => getSetting($db, 'aws_region', 'us-east-1'), + 'aws_hosted_zones' => getSetting($db, 'aws_hosted_zones', ''), + 'is_configured' => !empty(getSetting($db, 'aws_access_key_id', '')) && !empty(getSetting($db, 'aws_secret_access_key', '')) + ] + ]); +} + +/** + * Save AWS settings + */ +function handleAwsSettingsSave($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + saveSetting($db, 'aws_access_key_id', $input['aws_access_key_id'] ?? ''); + + // Only update secret if it's not masked + if (!empty($input['aws_secret_access_key']) && strpos($input['aws_secret_access_key'], '••••') === false) { + saveSetting($db, 'aws_secret_access_key', $input['aws_secret_access_key']); + } + + saveSetting($db, 'aws_region', $input['aws_region'] ?? 'us-east-1'); + saveSetting($db, 'aws_hosted_zones', $input['aws_hosted_zones'] ?? ''); + + jsonResponse(['success' => true, 'message' => 'AWS settings saved successfully']); +} + +/** + * Test AWS connection + */ +function handleAwsTest($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $input = json_decode(file_get_contents('php://input'), true); + + if (!validateCSRFToken($input['csrf_token'] ?? '')) { + jsonResponse(['error' => 'Invalid CSRF token'], 403); + } + + $accessKeyId = getSetting($db, 'aws_access_key_id', ''); + $secretAccessKey = getSetting($db, 'aws_secret_access_key', ''); + $region = getSetting($db, 'aws_region', 'us-east-1'); + + if (empty($accessKeyId) || empty($secretAccessKey)) { + jsonResponse(['error' => 'AWS credentials not configured'], 400); + } + + try { + $result = awsRoute53Request('GET', '2013-04-01/hostedzone', [], $accessKeyId, $secretAccessKey, $region); + + if (isset($result['error'])) { + jsonResponse(['success' => false, 'error' => $result['error']]); + } + + $zoneCount = 0; + if (isset($result['HostedZones']['HostedZone'])) { + $zones = $result['HostedZones']['HostedZone']; + $zoneCount = isset($zones['Id']) ? 1 : count($zones); + } + + jsonResponse([ + 'success' => true, + 'message' => "Connection successful! Found {$zoneCount} hosted zone(s)." + ]); + } catch (Exception $e) { + jsonResponse(['success' => false, 'error' => $e->getMessage()]); + } +} + +/** + * Get AWS hosted zones with details + */ +function handleAwsZones($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $accessKeyId = getSetting($db, 'aws_access_key_id', ''); + $secretAccessKey = getSetting($db, 'aws_secret_access_key', ''); + $region = getSetting($db, 'aws_region', 'us-east-1'); + $configuredZones = array_map('trim', explode(',', getSetting($db, 'aws_hosted_zones', ''))); + + if (empty($accessKeyId) || empty($secretAccessKey)) { + jsonResponse(['error' => 'AWS credentials not configured'], 400); + } + + try { + $result = awsRoute53Request('GET', '2013-04-01/hostedzone', [], $accessKeyId, $secretAccessKey, $region); + + if (isset($result['error'])) { + jsonResponse(['success' => false, 'error' => $result['error']]); + } + + $zones = []; + if (isset($result['HostedZones']['HostedZone'])) { + $hostedZones = $result['HostedZones']['HostedZone']; + // Handle single zone vs array of zones + if (isset($hostedZones['Id'])) { + $hostedZones = [$hostedZones]; + } + + foreach ($hostedZones as $zone) { + $zoneId = str_replace('/hostedzone/', '', $zone['Id']); + // Only include configured zones if any are specified + if (empty($configuredZones[0]) || in_array($zoneId, $configuredZones)) { + $zones[] = [ + 'id' => $zoneId, + 'name' => rtrim($zone['Name'], '.'), + 'record_count' => $zone['ResourceRecordSetCount'] ?? 0, + 'private' => ($zone['Config']['PrivateZone'] ?? 'false') === 'true' + ]; + } + } + } + + jsonResponse(['success' => true, 'zones' => $zones]); + } catch (Exception $e) { + jsonResponse(['success' => false, 'error' => $e->getMessage()]); + } +} + +/** + * Get A records from a hosted zone + */ +function handleAwsRecords($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $zoneId = $_GET['zone_id'] ?? ''; + if (empty($zoneId)) { + jsonResponse(['error' => 'Zone ID required'], 400); + } + + $accessKeyId = getSetting($db, 'aws_access_key_id', ''); + $secretAccessKey = getSetting($db, 'aws_secret_access_key', ''); + $region = getSetting($db, 'aws_region', 'us-east-1'); + + if (empty($accessKeyId) || empty($secretAccessKey)) { + jsonResponse(['error' => 'AWS credentials not configured'], 400); + } + + try { + $result = awsRoute53Request( + 'GET', + "2013-04-01/hostedzone/{$zoneId}/rrset?type=A&maxitems=300", + [], + $accessKeyId, + $secretAccessKey, + $region + ); + + if (isset($result['error'])) { + jsonResponse(['success' => false, 'error' => $result['error']]); + } + + $records = []; + if (isset($result['ResourceRecordSets']['ResourceRecordSet'])) { + $recordSets = $result['ResourceRecordSets']['ResourceRecordSet']; + // Handle single record vs array + if (isset($recordSets['Name'])) { + $recordSets = [$recordSets]; + } + + foreach ($recordSets as $record) { + if (($record['Type'] ?? '') !== 'A') continue; + + $ips = []; + if (isset($record['ResourceRecords']['ResourceRecord'])) { + $rrs = $record['ResourceRecords']['ResourceRecord']; + if (isset($rrs['Value'])) { + $ips[] = $rrs['Value']; + } else { + foreach ($rrs as $rr) { + $ips[] = $rr['Value']; + } + } + } + + foreach ($ips as $ip) { + $records[] = [ + 'hostname' => rtrim($record['Name'], '.'), + 'ip' => $ip, + 'ttl' => $record['TTL'] ?? 300, + 'ptr' => null, + 'ptr_status' => 'unknown' + ]; + } + } + } + + jsonResponse(['success' => true, 'records' => $records]); + } catch (Exception $e) { + jsonResponse(['success' => false, 'error' => $e->getMessage()]); + } +} + +/** + * Lookup PTR record for an IP + */ +function handlePtrLookup($db) { + if ($_SERVER['REQUEST_METHOD'] !== 'GET') { + jsonResponse(['error' => 'Method not allowed'], 405); + } + + $ip = $_GET['ip'] ?? ''; + $expectedHostname = $_GET['hostname'] ?? ''; + + if (empty($ip)) { + jsonResponse(['error' => 'IP address required'], 400); + } + + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + jsonResponse(['error' => 'Invalid IPv4 address'], 400); + } + + try { + // Perform reverse DNS lookup + $ptr = gethostbyaddr($ip); + + // If gethostbyaddr returns the IP, no PTR was found + if ($ptr === $ip) { + $ptr = null; + } + + // Determine status + $status = 'unknown'; + if ($ptr === null) { + $status = 'missing'; + } elseif (!empty($expectedHostname)) { + // Normalize hostnames for comparison (remove trailing dots, lowercase) + $normalizedPtr = strtolower(rtrim($ptr, '.')); + $normalizedExpected = strtolower(rtrim($expectedHostname, '.')); + + if ($normalizedPtr === $normalizedExpected) { + $status = 'match'; + } else { + $status = 'mismatch'; + } + } else { + $status = 'found'; + } + + jsonResponse([ + 'success' => true, + 'ip' => $ip, + 'ptr' => $ptr, + 'status' => $status, + 'expected' => $expectedHostname + ]); + } catch (Exception $e) { + jsonResponse(['success' => false, 'error' => $e->getMessage()]); + } +} + +/** + * Make AWS Route53 API request with Signature Version 4 + */ +function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey, $region) { + $service = 'route53'; + $host = 'route53.amazonaws.com'; + $endpoint = "https://{$host}/{$path}"; + + $algorithm = 'AWS4-HMAC-SHA256'; + $timestamp = gmdate('Ymd\THis\Z'); + $datestamp = gmdate('Ymd'); + + // Create canonical request + $canonicalUri = '/' . $path; + $canonicalQuerystring = ''; + + // Parse query string if present + if (strpos($path, '?') !== false) { + list($canonicalUri, $queryString) = explode('?', '/' . $path, 2); + parse_str($queryString, $queryParams); + ksort($queryParams); + $canonicalQuerystring = http_build_query($queryParams); + } + + $payloadHash = hash('sha256', is_array($body) ? json_encode($body) : ''); + + $canonicalHeaders = "host:{$host}\n" . + "x-amz-date:{$timestamp}\n"; + $signedHeaders = 'host;x-amz-date'; + + $canonicalRequest = "{$method}\n{$canonicalUri}\n{$canonicalQuerystring}\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}"; + + // Create string to sign + $credentialScope = "{$datestamp}/{$region}/{$service}/aws4_request"; + $stringToSign = "{$algorithm}\n{$timestamp}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest); + + // Calculate signature + $kSecret = 'AWS4' . $secretAccessKey; + $kDate = hash_hmac('sha256', $datestamp, $kSecret, true); + $kRegion = hash_hmac('sha256', $region, $kDate, true); + $kService = hash_hmac('sha256', $service, $kRegion, true); + $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); + $signature = hash_hmac('sha256', $stringToSign, $kSigning); + + // Create authorization header + $authorizationHeader = "{$algorithm} Credential={$accessKeyId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}"; + + // Make request + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $endpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [ + "Host: {$host}", + "X-Amz-Date: {$timestamp}", + "Authorization: {$authorizationHeader}", + "Content-Type: application/xml" + ] + ]); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($body) ? json_encode($body) : $body); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error) { + return ['error' => "cURL error: {$error}"]; + } + + if ($httpCode >= 400) { + // Parse error from XML response + if (preg_match('/(.+?)<\/Message>/s', $response, $matches)) { + return ['error' => $matches[1]]; + } + return ['error' => "HTTP {$httpCode}: {$response}"]; + } + + // Parse XML response + $xml = simplexml_load_string($response); + if ($xml === false) { + return ['error' => 'Failed to parse XML response']; + } + + return json_decode(json_encode($xml), true); +} diff --git a/webapp/index.php b/webapp/index.php index 87beb4f..497afdb 100644 --- a/webapp/index.php +++ b/webapp/index.php @@ -1556,6 +1556,12 @@ if (function_exists('requireAuth')) { Advanced + + + + + + +

Danger Zone

Irreversible actions - please proceed with caution.

@@ -1908,6 +1977,109 @@ if (function_exists('requireAuth')) {
+ +
+
+

+ + + + + + PTR Record Management +

+

View A records from your Route53 hosted zones and check their PTR records at IPXO.

+ + + +
+
+
+ + +
+ + +
+ +
+
+
+

A Records

+ +
+
+ + + + + + + + + + + + + + + + +
HostnameIP AddressTTLCurrent PTRPTR StatusActions
+ Select a hosted zone to view A records +
+
+
+
+
+
+ + +
+

PTR Status Legend

+
+
+ MATCH + PTR matches hostname +
+
+ MISMATCH + PTR exists but doesn't match +
+
+ MISSING + No PTR record found +
+
+ UNKNOWN + Not checked yet +
+
+
+
+
@@ -2246,6 +2418,7 @@ if (function_exists('requireAuth')) { loadWebhookSettings(); loadWebhookQueueStatus(); loadIpRegistrySettings(); + loadAwsSettings(); } // Load data for developer tab @@ -2253,6 +2426,11 @@ if (function_exists('requireAuth')) { loadSystemInfo(); loadErrorLogs(); } + + // Load data for PTR tab + if (tab === 'ptr') { + loadPtrZones(); + } } // API Helper @@ -4036,6 +4214,298 @@ if (function_exists('requireAuth')) { } } + // ============================================= + // AWS Route53 / PTR Record Functions + // ============================================= + + // Load AWS settings + async function loadAwsSettings() { + try { + const result = await api('aws_settings_get'); + if (result.success && result.data) { + document.getElementById('awsAccessKeyId').value = result.data.aws_access_key_id || ''; + document.getElementById('awsSecretAccessKey').value = result.data.aws_secret_access_key || ''; + document.getElementById('awsRegion').value = result.data.aws_region || 'us-east-1'; + document.getElementById('awsHostedZones').value = result.data.aws_hosted_zones || ''; + } + } catch (error) { + console.error('Failed to load AWS settings:', error); + } + } + + // Save AWS settings + async function saveAwsSettings() { + const data = { + aws_access_key_id: document.getElementById('awsAccessKeyId').value, + aws_secret_access_key: document.getElementById('awsSecretAccessKey').value, + aws_region: document.getElementById('awsRegion').value, + aws_hosted_zones: document.getElementById('awsHostedZones').value + }; + + try { + const result = await api('aws_settings_save', {}, 'POST', data); + if (result.success) { + showToast('AWS settings saved successfully', 'success'); + } else { + showToast(result.error || 'Failed to save AWS settings', 'error'); + } + } catch (error) { + showToast('Failed to save AWS settings: ' + error.message, 'error'); + } + } + + // Test AWS connection + async function testAwsConnection() { + const resultDiv = document.getElementById('awsTestResult'); + resultDiv.style.display = 'block'; + resultDiv.innerHTML = '
Testing connection...
'; + + try { + const result = await api('aws_test'); + if (result.success) { + resultDiv.innerHTML = ` +
+ + + + Connection successful! Found ${result.zones_count} hosted zone(s). +
+ `; + } else { + resultDiv.innerHTML = ` +
+ + + + + + ${escapeHtml(result.error || 'Connection failed')} +
+ `; + } + } catch (error) { + resultDiv.innerHTML = ` +
+ + + + + + Connection failed: ${escapeHtml(error.message)} +
+ `; + } + } + + // Load PTR zones into dropdown + async function loadPtrZones() { + const select = document.getElementById('ptrZoneSelect'); + const notConfigured = document.getElementById('ptrNotConfigured'); + const configured = document.getElementById('ptrConfigured'); + + try { + const result = await api('aws_zones'); + if (result.success && result.zones && result.zones.length > 0) { + notConfigured.style.display = 'none'; + configured.style.display = 'block'; + + select.innerHTML = ''; + result.zones.forEach(zone => { + const option = document.createElement('option'); + option.value = zone.id; + option.textContent = zone.name + ' (' + zone.id + ')'; + select.appendChild(option); + }); + } else { + notConfigured.style.display = 'block'; + configured.style.display = 'none'; + } + } catch (error) { + notConfigured.style.display = 'block'; + configured.style.display = 'none'; + console.error('Failed to load PTR zones:', error); + } + } + + // Load A records from selected zone + async function loadPtrRecords() { + const zoneId = document.getElementById('ptrZoneSelect').value; + const tbody = document.getElementById('ptrRecordsBody'); + const countSpan = document.getElementById('ptrRecordCount'); + const checkAllBtn = document.getElementById('checkAllPtrsBtn'); + + if (!zoneId) { + tbody.innerHTML = ` + + + Select a hosted zone to view A records + + + `; + countSpan.textContent = ''; + checkAllBtn.disabled = true; + return; + } + + tbody.innerHTML = ` + + +
+ + + `; + + try { + const result = await api('aws_records', { zone_id: zoneId }); + if (result.success) { + const records = result.records || []; + countSpan.textContent = records.length + ' record(s)'; + checkAllBtn.disabled = records.length === 0; + + if (records.length === 0) { + tbody.innerHTML = ` + + + No A records found in this zone + + + `; + return; + } + + tbody.innerHTML = records.map((record, index) => ` + + ${escapeHtml(record.hostname)} + ${escapeHtml(record.ip)} + ${record.ttl || '-'} + - + + UNKNOWN + + + + + + `).join(''); + } else { + tbody.innerHTML = ` + + + ${escapeHtml(result.error || 'Failed to load records')} + + + `; + countSpan.textContent = ''; + checkAllBtn.disabled = true; + } + } catch (error) { + tbody.innerHTML = ` + + + Failed to load records: ${escapeHtml(error.message)} + + + `; + countSpan.textContent = ''; + checkAllBtn.disabled = true; + } + } + + // Check single PTR record + async function checkPtrRecord(ip, expectedHostname, button) { + const row = button.closest('tr'); + const ptrValueCell = row.querySelector('.ptr-value'); + const ptrStatusCell = row.querySelector('.ptr-status'); + + button.disabled = true; + button.innerHTML = '
'; + + try { + const result = await api('ptr_lookup', { ip: ip }); + if (result.success) { + const ptr = result.ptr || ''; + ptrValueCell.textContent = ptr || '(none)'; + ptrValueCell.style.color = ptr ? 'var(--text-primary)' : 'var(--text-tertiary)'; + + // Normalize hostnames for comparison (remove trailing dot, lowercase) + const normalizedPtr = ptr.toLowerCase().replace(/\.$/, ''); + const normalizedExpected = expectedHostname.toLowerCase().replace(/\.$/, ''); + + let status, statusColor; + if (!ptr) { + status = 'MISSING'; + statusColor = 'var(--error)'; + } else if (normalizedPtr === normalizedExpected) { + status = 'MATCH'; + statusColor = 'var(--success)'; + } else { + status = 'MISMATCH'; + statusColor = 'var(--warning)'; + } + + ptrStatusCell.innerHTML = `${status}`; + } else { + ptrValueCell.textContent = 'Error'; + ptrValueCell.style.color = 'var(--error)'; + ptrStatusCell.innerHTML = `ERROR`; + } + } catch (error) { + ptrValueCell.textContent = 'Error'; + ptrValueCell.style.color = 'var(--error)'; + ptrStatusCell.innerHTML = `ERROR`; + } + + button.disabled = false; + button.innerHTML = ` + + + + Check + `; + } + + // Check all PTR records + async function checkAllPtrs() { + const rows = document.querySelectorAll('#ptrRecordsBody tr[data-ip]'); + const checkAllBtn = document.getElementById('checkAllPtrsBtn'); + + if (rows.length === 0) { + showToast('No records to check', 'warning'); + return; + } + + checkAllBtn.disabled = true; + checkAllBtn.innerHTML = '
Checking...'; + + let checked = 0; + for (const row of rows) { + const ip = row.getAttribute('data-ip'); + const hostname = row.getAttribute('data-hostname'); + const button = row.querySelector('button'); + + await checkPtrRecord(ip, hostname, button); + checked++; + + // Small delay to avoid overwhelming the server + await new Promise(resolve => setTimeout(resolve, 100)); + } + + checkAllBtn.disabled = false; + checkAllBtn.innerHTML = ` + + + + Check All PTRs + `; + + showToast(`Checked ${checked} PTR record(s)`, 'success'); + } + // Clear error logs async function clearErrorLogs() { if (!confirm('Are you sure you want to clear the error log? This action cannot be undone.')) {