fix width

This commit is contained in:
Purple
2026-01-17 23:20:33 +00:00
parent fafb536be4
commit a971f71618
4 changed files with 967 additions and 1 deletions

View File

@@ -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.

View File

@@ -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;
-- ============================================

View File

@@ -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>(.+?)<\/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);
}

View File

@@ -1556,6 +1556,12 @@ if (function_exists('requireAuth')) {
</svg>
Advanced
</button>
<button class="tab" onclick="switchTab('ptr')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
PTR Records
</button>
<button class="tab" onclick="switchTab('developer')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
@@ -1894,6 +1900,69 @@ if (function_exists('requireAuth')) {
</div>
</div>
<!-- AWS Route53 Settings Section -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
AWS Route53 Settings
</h2>
<p class="advanced-section-desc">Configure AWS credentials and hosted zones for PTR record management.</p>
<div class="form-grid" style="margin-top: 16px;">
<div class="form-group">
<label class="form-label">AWS Access Key ID</label>
<input type="text" class="form-input" id="awsAccessKeyId" placeholder="AKIAIOSFODNN7EXAMPLE">
</div>
<div class="form-group">
<label class="form-label">AWS Secret Access Key</label>
<input type="password" class="form-input" id="awsSecretAccessKey" placeholder="••••••••••••••••">
</div>
</div>
<div class="form-group">
<label class="form-label">AWS Region</label>
<select class="form-select" id="awsRegion">
<option value="us-east-1">US East (N. Virginia)</option>
<option value="us-east-2">US East (Ohio)</option>
<option value="us-west-1">US West (N. California)</option>
<option value="us-west-2">US West (Oregon)</option>
<option value="eu-west-1">EU (Ireland)</option>
<option value="eu-west-2">EU (London)</option>
<option value="eu-central-1">EU (Frankfurt)</option>
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Route53 Hosted Zone IDs (comma separated)</label>
<input type="text" class="form-input" id="awsHostedZones" placeholder="Z1234567890ABC, Z0987654321DEF">
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">Enter the hosted zone IDs for your forward DNS zones (A records)</small>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
<button class="btn btn-primary" onclick="saveAwsSettings()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save AWS Settings
</button>
<button class="btn btn-secondary" onclick="testAwsConnection()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
Test Connection
</button>
</div>
<div id="awsTestResult" style="margin-top: 16px; display: none;"></div>
</div>
<div class="advanced-section">
<h2 class="advanced-section-title">Danger Zone</h2>
<p class="advanced-section-desc">Irreversible actions - please proceed with caution.</p>
@@ -1908,6 +1977,109 @@ if (function_exists('requireAuth')) {
</div>
</div>
<!-- PTR Records Tab -->
<div class="tab-content" id="tab-ptr">
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
PTR Record Management
</h2>
<p class="advanced-section-desc">View A records from your Route53 hosted zones and check their PTR records at IPXO.</p>
<div id="ptrNotConfigured" style="display: none;">
<div class="alert alert-warning">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span>AWS Route53 is not configured. Please configure your AWS credentials and hosted zones in the <a href="#" onclick="switchTab('advanced'); return false;" style="color: var(--purple-primary);">Advanced tab</a>.</span>
</div>
</div>
<div id="ptrConfigured">
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; align-items: center;">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
<label class="form-label">Select Hosted Zone</label>
<select class="form-select" id="ptrZoneSelect" onchange="loadPtrRecords()">
<option value="">Select a zone...</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadPtrRecords()" style="margin-top: 24px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh
</button>
<button class="btn btn-secondary" onclick="checkAllPtrs()" style="margin-top: 24px;" id="checkAllPtrsBtn" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
Check All PTRs
</button>
</div>
<div id="ptrRecordsContainer">
<div class="table-container">
<div class="table-header">
<h3 class="table-title">A Records</h3>
<span id="ptrRecordCount" style="color: var(--text-secondary); font-size: 13px;"></span>
</div>
<div class="table-scroll">
<table style="min-width: 900px;">
<thead>
<tr>
<th style="min-width: 250px;">Hostname</th>
<th style="width: 140px;">IP Address</th>
<th style="width: 80px;">TTL</th>
<th style="min-width: 250px;">Current PTR</th>
<th style="width: 100px;">PTR Status</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody id="ptrRecordsBody">
<tr>
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
Select a hosted zone to view A records
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- PTR Legend -->
<div class="advanced-section">
<h2 class="advanced-section-title">PTR Status Legend</h2>
<div style="display: flex; gap: 24px; flex-wrap: wrap; margin-top: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--success); color: white;">MATCH</span>
<span style="font-size: 13px; color: var(--text-secondary);">PTR matches hostname</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--warning); color: white;">MISMATCH</span>
<span style="font-size: 13px; color: var(--text-secondary);">PTR exists but doesn't match</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">MISSING</span>
<span style="font-size: 13px; color: var(--text-secondary);">No PTR record found</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--text-tertiary); color: white;">UNKNOWN</span>
<span style="font-size: 13px; color: var(--text-secondary);">Not checked yet</span>
</div>
</div>
</div>
</div>
<!-- Developer Tab -->
<div class="tab-content" id="tab-developer">
<!-- Database Backup Section -->
@@ -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 = '<div class="loading"><div class="spinner"></div> Testing connection...</div>';
try {
const result = await api('aws_test');
if (result.success) {
resultDiv.innerHTML = `
<div class="alert alert-success">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>Connection successful! Found ${result.zones_count} hosted zone(s).</span>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span>${escapeHtml(result.error || 'Connection failed')}</span>
</div>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<span>Connection failed: ${escapeHtml(error.message)}</span>
</div>
`;
}
}
// 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 = '<option value="">Select a zone...</option>';
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 = `
<tr>
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
Select a hosted zone to view A records
</td>
</tr>
`;
countSpan.textContent = '';
checkAllBtn.disabled = true;
return;
}
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; padding: 40px;">
<div class="loading"><div class="spinner"></div></div>
</td>
</tr>
`;
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 = `
<tr>
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
No A records found in this zone
</td>
</tr>
`;
return;
}
tbody.innerHTML = records.map((record, index) => `
<tr data-ip="${escapeHtml(record.ip)}" data-hostname="${escapeHtml(record.hostname)}">
<td><span class="cell-truncate" title="${escapeHtml(record.hostname)}">${escapeHtml(record.hostname)}</span></td>
<td><code style="font-size: 12px;">${escapeHtml(record.ip)}</code></td>
<td style="color: var(--text-secondary);">${record.ttl || '-'}</td>
<td class="ptr-value" style="color: var(--text-tertiary);">-</td>
<td class="ptr-status">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--text-tertiary); color: white;">UNKNOWN</span>
</td>
<td>
<button class="btn btn-secondary btn-sm" onclick="checkPtrRecord('${escapeHtml(record.ip)}', '${escapeHtml(record.hostname)}', this)" title="Check PTR">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
Check
</button>
</td>
</tr>
`).join('');
} else {
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; color: var(--error); padding: 40px;">
${escapeHtml(result.error || 'Failed to load records')}
</td>
</tr>
`;
countSpan.textContent = '';
checkAllBtn.disabled = true;
}
} catch (error) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; color: var(--error); padding: 40px;">
Failed to load records: ${escapeHtml(error.message)}
</td>
</tr>
`;
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 = '<div class="spinner" style="width: 14px; height: 14px;"></div>';
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 = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${status}</span>`;
} else {
ptrValueCell.textContent = 'Error';
ptrValueCell.style.color = 'var(--error)';
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">ERROR</span>`;
}
} catch (error) {
ptrValueCell.textContent = 'Error';
ptrValueCell.style.color = 'var(--error)';
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">ERROR</span>`;
}
button.disabled = false;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
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 = '<div class="spinner" style="width: 14px; height: 14px;"></div> 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 = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
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.')) {