fix aws
This commit is contained in:
171
webapp/api.php
171
webapp/api.php
@@ -218,6 +218,10 @@ try {
|
||||
handleWhitelabelSave($db);
|
||||
break;
|
||||
|
||||
case 'aws_update_a_record':
|
||||
handleAwsUpdateARecord($db);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Invalid action'], 400);
|
||||
}
|
||||
@@ -2496,7 +2500,7 @@ function handlePtrLookup($db) {
|
||||
* Make AWS Route53 API request with Signature Version 4
|
||||
* Note: Route53 is a global service, always uses us-east-1 for signing
|
||||
*/
|
||||
function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey, $region) {
|
||||
function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey, $region, $contentType = null) {
|
||||
$service = 'route53';
|
||||
$host = 'route53.amazonaws.com';
|
||||
// Route53 is a global service, always use us-east-1 for signing
|
||||
@@ -2533,9 +2537,23 @@ function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey
|
||||
$payloadHash = hash('sha256', $payload);
|
||||
|
||||
// Build canonical headers - must be sorted alphabetically and lowercase
|
||||
$canonicalHeaders = "host:{$host}\n" .
|
||||
"x-amz-date:{$timestamp}\n";
|
||||
$signedHeaders = 'host;x-amz-date';
|
||||
$canonicalHeaders = '';
|
||||
$signedHeadersList = ['host', 'x-amz-date'];
|
||||
|
||||
if ($contentType) {
|
||||
$canonicalHeaders .= "content-type:{$contentType}\n";
|
||||
$signedHeadersList[] = 'content-type';
|
||||
}
|
||||
$canonicalHeaders .= "host:{$host}\n";
|
||||
$canonicalHeaders .= "x-amz-date:{$timestamp}\n";
|
||||
|
||||
sort($signedHeadersList);
|
||||
$signedHeaders = implode(';', $signedHeadersList);
|
||||
|
||||
// Sort canonical headers alphabetically
|
||||
$headerLines = explode("\n", trim($canonicalHeaders));
|
||||
sort($headerLines);
|
||||
$canonicalHeaders = implode("\n", $headerLines) . "\n";
|
||||
|
||||
// Build canonical request
|
||||
$canonicalRequest = implode("\n", [
|
||||
@@ -2567,17 +2585,24 @@ function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey
|
||||
// Create authorization header
|
||||
$authorizationHeader = "{$algorithm} Credential={$accessKeyId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
|
||||
|
||||
// Build HTTP headers
|
||||
$httpHeaders = [
|
||||
"Host: {$host}",
|
||||
"X-Amz-Date: {$timestamp}",
|
||||
"Authorization: {$authorizationHeader}"
|
||||
];
|
||||
|
||||
if ($contentType) {
|
||||
$httpHeaders[] = "Content-Type: {$contentType}";
|
||||
}
|
||||
|
||||
// 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}"
|
||||
]
|
||||
CURLOPT_HTTPHEADER => $httpHeaders
|
||||
]);
|
||||
|
||||
if ($method === 'POST') {
|
||||
@@ -2941,3 +2966,131 @@ function handleWhitelabelSave($db) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update A record in Route53
|
||||
*/
|
||||
function handleAwsUpdateARecord($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
// Verify CSRF token
|
||||
if (!isset($input['csrf_token']) || !verifyCsrfToken($input['csrf_token'])) {
|
||||
jsonResponse(['error' => 'Invalid CSRF token'], 403);
|
||||
}
|
||||
|
||||
$zoneId = $input['zone_id'] ?? '';
|
||||
$oldHostname = $input['old_hostname'] ?? '';
|
||||
$hostname = $input['hostname'] ?? '';
|
||||
$ip = $input['ip'] ?? '';
|
||||
$ttl = intval($input['ttl'] ?? 3600);
|
||||
|
||||
if (empty($zoneId) || empty($hostname) || empty($ip)) {
|
||||
jsonResponse(['error' => 'Zone ID, hostname, and IP are 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 {
|
||||
$cleanZoneId = preg_replace('/^\/hostedzone\//', '', $zoneId);
|
||||
|
||||
// Ensure hostname ends with a dot (FQDN)
|
||||
$fqdnHostname = rtrim($hostname, '.') . '.';
|
||||
$fqdnOldHostname = rtrim($oldHostname, '.') . '.';
|
||||
|
||||
// Build the ChangeResourceRecordSets XML request
|
||||
$changes = [];
|
||||
|
||||
// If hostname changed, we need to DELETE the old and CREATE the new
|
||||
if ($oldHostname && $oldHostname !== $hostname) {
|
||||
// Delete old record
|
||||
$changes[] = [
|
||||
'Action' => 'DELETE',
|
||||
'Name' => $fqdnOldHostname,
|
||||
'Type' => 'A',
|
||||
'TTL' => $ttl,
|
||||
'Value' => $ip
|
||||
];
|
||||
// Create new record
|
||||
$changes[] = [
|
||||
'Action' => 'CREATE',
|
||||
'Name' => $fqdnHostname,
|
||||
'Type' => 'A',
|
||||
'TTL' => $ttl,
|
||||
'Value' => $ip
|
||||
];
|
||||
} else {
|
||||
// Just UPSERT the record
|
||||
$changes[] = [
|
||||
'Action' => 'UPSERT',
|
||||
'Name' => $fqdnHostname,
|
||||
'Type' => 'A',
|
||||
'TTL' => $ttl,
|
||||
'Value' => $ip
|
||||
];
|
||||
}
|
||||
|
||||
// Build XML body
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
$xml .= '<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">';
|
||||
$xml .= '<ChangeBatch>';
|
||||
$xml .= '<Comment>Updated via Geofeed Manager</Comment>';
|
||||
$xml .= '<Changes>';
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$xml .= '<Change>';
|
||||
$xml .= '<Action>' . $change['Action'] . '</Action>';
|
||||
$xml .= '<ResourceRecordSet>';
|
||||
$xml .= '<Name>' . htmlspecialchars($change['Name']) . '</Name>';
|
||||
$xml .= '<Type>' . $change['Type'] . '</Type>';
|
||||
$xml .= '<TTL>' . $change['TTL'] . '</TTL>';
|
||||
$xml .= '<ResourceRecords>';
|
||||
$xml .= '<ResourceRecord>';
|
||||
$xml .= '<Value>' . htmlspecialchars($change['Value']) . '</Value>';
|
||||
$xml .= '</ResourceRecord>';
|
||||
$xml .= '</ResourceRecords>';
|
||||
$xml .= '</ResourceRecordSet>';
|
||||
$xml .= '</Change>';
|
||||
}
|
||||
|
||||
$xml .= '</Changes>';
|
||||
$xml .= '</ChangeBatch>';
|
||||
$xml .= '</ChangeResourceRecordSetsRequest>';
|
||||
|
||||
// Make the Route53 API call
|
||||
$result = awsRoute53Request(
|
||||
'POST',
|
||||
"2013-04-01/hostedzone/{$cleanZoneId}/rrset",
|
||||
$xml,
|
||||
$accessKeyId,
|
||||
$secretAccessKey,
|
||||
$region,
|
||||
'application/xml'
|
||||
);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
jsonResponse(['success' => false, 'error' => $result['error']]);
|
||||
}
|
||||
|
||||
// Update the local cache
|
||||
$stmt = $db->prepare("
|
||||
UPDATE ptr_records_cache
|
||||
SET hostname = ?, ttl = ?, updated_at = NOW()
|
||||
WHERE zone_id = ? AND ip_address = ?
|
||||
");
|
||||
$stmt->execute([$hostname, $ttl, $cleanZoneId, $ip]);
|
||||
|
||||
jsonResponse(['success' => true, 'message' => 'A record updated successfully']);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
181
webapp/index.php
181
webapp/index.php
@@ -2618,6 +2618,48 @@ if (function_exists('requireAuth')) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit A Record Modal -->
|
||||
<div class="modal-overlay" id="editARecordModal">
|
||||
<div class="modal" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Edit A Record</h2>
|
||||
<p class="modal-subtitle">Update the A record in Route53</p>
|
||||
</div>
|
||||
<form id="editARecordForm" onsubmit="saveARecord(event)">
|
||||
<input type="hidden" id="editARecordId">
|
||||
<input type="hidden" id="editARecordOldHostname">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Hostname <span class="required">*</span></label>
|
||||
<input type="text" class="form-input" id="editARecordHostname" required>
|
||||
<div class="form-hint">The DNS hostname (e.g., server1.example.com)</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">IP Address</label>
|
||||
<input type="text" class="form-input" id="editARecordIp" readonly style="background: var(--bg-tertiary); cursor: not-allowed;">
|
||||
<div class="form-hint">IP address cannot be changed</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">TTL (seconds)</label>
|
||||
<input type="number" class="form-input" id="editARecordTtl" min="60" max="86400" value="3600">
|
||||
<div class="form-hint">Time to live (60-86400 seconds)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeEditARecordModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="saveARecordBtn">
|
||||
<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>
|
||||
Update A Record
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
@@ -4797,24 +4839,33 @@ if (function_exists('requireAuth')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store records globally for IPXO export
|
||||
window.cachedPtrRecords = records;
|
||||
document.getElementById('exportIpxoBtn').disabled = !records.some(r => r.ptr_status === 'mismatch' || r.ptr_status === 'missing');
|
||||
|
||||
tbody.innerHTML = records.map((record) => {
|
||||
const statusColor = getStatusColor(record.ptr_status);
|
||||
const statusLabel = (record.ptr_status || 'unknown').toUpperCase();
|
||||
return `
|
||||
<tr data-id="${record.id}" data-ip="${escapeHtml(record.ip_address)}" data-hostname="${escapeHtml(record.hostname)}">
|
||||
<td><span class="cell-truncate" title="${escapeHtml(record.hostname)}">${escapeHtml(record.hostname)}</span></td>
|
||||
<tr data-id="${record.id}" data-ip="${escapeHtml(record.ip_address)}" data-hostname="${escapeHtml(record.hostname)}" data-ttl="${record.ttl || 3600}" data-zone-id="${escapeHtml(record.zone_id || '')}">
|
||||
<td><code style="font-size: 12px;">${escapeHtml(record.ip_address)}</code></td>
|
||||
<td><span class="cell-truncate" title="${escapeHtml(record.hostname)}">${escapeHtml(record.hostname)}</span></td>
|
||||
<td style="color: var(--text-secondary);">${record.ttl || '-'}</td>
|
||||
<td class="ptr-value" style="color: ${record.ptr_record ? 'var(--text-primary)' : 'var(--text-tertiary)'};">${record.ptr_record || '-'}</td>
|
||||
<td class="ptr-status">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${statusLabel}</span>
|
||||
</td>
|
||||
<td>
|
||||
<td style="display: flex; gap: 4px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="checkSinglePtr(${record.id}, 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>
|
||||
<button class="btn btn-ghost btn-sm" onclick="editARecord(${record.id}, '${escapeHtml(record.hostname)}', '${escapeHtml(record.ip_address)}', ${record.ttl || 3600})" title="Edit A Record">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -4974,6 +5025,128 @@ if (function_exists('requireAuth')) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Edit A Record modal functions
|
||||
function editARecord(id, hostname, ip, ttl) {
|
||||
document.getElementById('editARecordId').value = id;
|
||||
document.getElementById('editARecordOldHostname').value = hostname;
|
||||
document.getElementById('editARecordHostname').value = hostname;
|
||||
document.getElementById('editARecordIp').value = ip;
|
||||
document.getElementById('editARecordTtl').value = ttl || 3600;
|
||||
document.getElementById('editARecordModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeEditARecordModal() {
|
||||
document.getElementById('editARecordModal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function saveARecord(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const zoneId = document.getElementById('ptrZoneSelect').value;
|
||||
if (!zoneId) {
|
||||
showToast('No zone selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = document.getElementById('editARecordId').value;
|
||||
const oldHostname = document.getElementById('editARecordOldHostname').value;
|
||||
const hostname = document.getElementById('editARecordHostname').value;
|
||||
const ip = document.getElementById('editARecordIp').value;
|
||||
const ttl = parseInt(document.getElementById('editARecordTtl').value) || 3600;
|
||||
|
||||
const saveBtn = document.getElementById('saveARecordBtn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div> Updating...';
|
||||
|
||||
try {
|
||||
const result = await api('aws_update_a_record', {}, 'POST', {
|
||||
zone_id: zoneId,
|
||||
old_hostname: oldHostname,
|
||||
hostname: hostname,
|
||||
ip: ip,
|
||||
ttl: ttl
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
showToast('A record updated successfully', 'success');
|
||||
closeEditARecordModal();
|
||||
// Refresh the records from AWS
|
||||
await refreshFromAws();
|
||||
} else {
|
||||
showToast(result.error || 'Failed to update A record', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to update A record: ' + error.message, 'error');
|
||||
}
|
||||
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = `
|
||||
<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>
|
||||
Update A Record
|
||||
`;
|
||||
}
|
||||
|
||||
// Export IPXO JSON for mismatched/missing PTRs
|
||||
function exportIpxoJson() {
|
||||
const records = window.cachedPtrRecords || [];
|
||||
const mismatchedOrMissing = records.filter(r => r.ptr_status === 'mismatch' || r.ptr_status === 'missing');
|
||||
|
||||
if (mismatchedOrMissing.length === 0) {
|
||||
showToast('No mismatched or missing PTR records to export', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Group records by subnet (calculate /24 for IPv4)
|
||||
const subnets = {};
|
||||
mismatchedOrMissing.forEach(record => {
|
||||
const ip = record.ip_address;
|
||||
// Calculate /24 subnet (for IPv4)
|
||||
const parts = ip.split('.');
|
||||
if (parts.length === 4) {
|
||||
// Try /23 first by checking if this is an odd or even third octet
|
||||
const thirdOctet = parseInt(parts[2]);
|
||||
const subnetBase = thirdOctet % 2 === 0 ? thirdOctet : thirdOctet - 1;
|
||||
const subnet = `${parts[0]}.${parts[1]}.${subnetBase}.0/23`;
|
||||
|
||||
if (!subnets[subnet]) {
|
||||
subnets[subnet] = [];
|
||||
}
|
||||
subnets[subnet].push({
|
||||
address: ip,
|
||||
dname: record.hostname,
|
||||
ttl: record.ttl || 3600
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const ipxoData = {
|
||||
subnets: Object.keys(subnets).map(prefix => ({
|
||||
prefix: prefix,
|
||||
records: subnets[prefix]
|
||||
}))
|
||||
};
|
||||
|
||||
const jsonStr = JSON.stringify(ipxoData, null, 2);
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard.writeText(jsonStr).then(() => {
|
||||
showToast(`Copied ${mismatchedOrMissing.length} PTR record(s) to clipboard in IPXO format`, 'success');
|
||||
}).catch(err => {
|
||||
// Fallback for browsers that don't support clipboard API
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = jsonStr;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
showToast(`Copied ${mismatchedOrMissing.length} PTR record(s) to clipboard in IPXO format`, 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// Clear error logs
|
||||
async function clearErrorLogs() {
|
||||
if (!confirm('Are you sure you want to clear the error log? This action cannot be undone.')) {
|
||||
|
||||
Reference in New Issue
Block a user