Created
February 19, 2025 13:45
-
-
Save creativebastard/942ee59af0eff596489be51236fab146 to your computer and use it in GitHub Desktop.
Script to synchronize TLSA records between Stalwart mail server and CloudFlare. Might be buggy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // Config.json should look like this: | |
| // { | |
| // "domain_name": "DOMAIN", | |
| // "cf_api_key": "CFKEY", | |
| // "stalwart_api_key": "STALWARTKEY", | |
| // "stalwart_endpoint_url": "https://YOURSTALWARTURL" | |
| //} | |
| // Load JSON configuration with validation | |
| function loadConfig() { | |
| $configPath = 'config.json'; | |
| if (!file_exists($configPath) || !is_readable($configPath)) { | |
| echo "Error: $configPath not found or not readable.\n"; | |
| exit(1); | |
| } | |
| $config = json_decode(file_get_contents($configPath), true); | |
| if (json_last_error() !== JSON_ERROR_NONE) { | |
| echo "Error parsing $configPath: " . json_last_error_msg() . "\n"; | |
| exit(1); | |
| } | |
| $requiredKeys = ['domain_name', 'cf_api_key', 'stalwart_api_key', 'stalwart_endpoint_url']; | |
| foreach ($requiredKeys as $key) { | |
| if (!isset($config[$key]) || trim($config[$key]) === '') { | |
| echo "Error: Missing or empty value for '$key' in $configPath.\n"; | |
| exit(1); | |
| } | |
| } | |
| return array_map('trim', $config); | |
| } | |
| $config = loadConfig(); | |
| $domain = $config['domain_name']; | |
| $stalwartKey = $config['stalwart_api_key']; | |
| $stalwartUrl = rtrim($config['stalwart_endpoint_url'], '/') . "/api/dns/records/$domain"; | |
| $cfApiKey = $config['cf_api_key']; | |
| echo "Starting TLSA record synchronization for $domain...\n"; | |
| // Fetch TLSA records from Stalwart | |
| echo "Fetching TLSA records from Stalwart...\n"; | |
| $stalwartResponse = executeCurlRequest($stalwartUrl, 'GET', ['Authorization: Bearer ' . $stalwartKey]); | |
| if (!isset($stalwartResponse['data']) || empty($stalwartResponse['data'])) { | |
| echo "No TLSA records found in Stalwart. Exiting.\n"; | |
| exit(0); | |
| } | |
| $stalwartTlsa = array_filter($stalwartResponse['data'], fn($r) => $r['type'] === 'TLSA'); | |
| echo "Found " . count($stalwartTlsa) . " TLSA records from Stalwart.\n"; | |
| // Fetch Cloudflare Zone ID | |
| echo "Fetching Cloudflare zone ID for $domain...\n"; | |
| $cfZoneUrl = "https://api.cloudflare.com/client/v4/zones?name=$domain&status=active"; | |
| $cfZoneResponse = executeCurlRequest($cfZoneUrl, 'GET', ['Authorization: Bearer ' . $cfApiKey]); | |
| if (!$cfZoneResponse['success'] || empty($cfZoneResponse['result'])) { | |
| echo "Error: Zone not found for domain $domain.\n"; | |
| exit(1); | |
| } | |
| $zoneId = $cfZoneResponse['result'][0]['id']; | |
| echo "Zone ID: $zoneId.\n"; | |
| // Fetch existing DNS records from Cloudflare | |
| echo "Fetching existing DNS records from Cloudflare...\n"; | |
| $cfRecordsUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records"; | |
| $cfRecordsResponse = executeCurlRequest($cfRecordsUrl, 'GET', ['Authorization: Bearer ' . $cfApiKey]); | |
| if (!$cfRecordsResponse['success']) { | |
| echo "Error fetching Cloudflare DNS records: " . $cfRecordsResponse['errors'][0]['message'] . "\n"; | |
| exit(1); | |
| } | |
| // Rebuild existing TLSA records with normalized content | |
| $existingTlsa = []; | |
| foreach ($cfRecordsResponse['result'] as $record) { | |
| if ($record['type'] === 'TLSA') { | |
| $content = implode(' ', [ | |
| $record['data']['usage'], | |
| $record['data']['selector'], | |
| $record['data']['matching_type'], | |
| $record['data']['certificate'] | |
| ]); | |
| $existingTlsa[] = [ | |
| 'name' => $record['name'], | |
| 'content' => $content, | |
| 'id' => $record['id'] | |
| ]; | |
| } | |
| } | |
| echo "Found " . count($existingTlsa) . " TLSA records in Cloudflare.\n"; | |
| // Normalize records for comparison | |
| $stalwartNormalized = normalizeRecords($stalwartTlsa); | |
| $cfNormalized = normalizeRecords($existingTlsa); | |
| // Output normalized records for debugging | |
| echo "Comparing Stalwart and Cloudflare TLSA records...\n"; | |
| echo "Normalized Stalwart records:\n"; | |
| foreach ($stalwartNormalized as $record) { | |
| echo "- {$record['name']} ({$record['content']})\n"; | |
| } | |
| echo "\nNormalized Cloudflare records:\n"; | |
| foreach ($cfNormalized as $record) { | |
| echo "- {$record['name']} ({$record['content']})\n"; | |
| } | |
| // Check if records match | |
| if (compareRecords($stalwartNormalized, $cfNormalized)) { | |
| echo "\nTLSA records are already up to date.\n"; | |
| exit(0); | |
| } | |
| // Update records if mismatched | |
| echo "\nUpdating TLSA records...\n"; | |
| // Delete existing Cloudflare TLSA records | |
| if (!empty($existingTlsa)) { | |
| echo "Deleting existing Cloudflare TLSA records:\n"; | |
| foreach ($existingTlsa as $record) { | |
| $deleteUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/{$record['id']}"; | |
| $result = executeCurlRequest($deleteUrl, 'DELETE', ['Authorization: Bearer ' . $cfApiKey, 'Content-Type: application/json']); | |
| if ($result['success']) { | |
| echo "Deleted: {$record['name']} ({$record['content']})\n"; | |
| } else { | |
| echo "Delete failed: {$record['name']} - " . $result['errors'][0]['message'] . "\n"; | |
| } | |
| } | |
| } | |
| // Add new Stalwart TLSA records to Cloudflare | |
| echo "Adding updated TLSA records to Cloudflare:\n"; | |
| foreach ($stalwartNormalized as $stalwartRecord) { | |
| // Split content into components | |
| $contentParts = explode(' ', trim($stalwartRecord['content']), 4); | |
| if (count($contentParts) !== 4) { | |
| echo "Invalid TLSA format for {$stalwartRecord['name']}: " . $stalwartRecord['content'] . "\n"; | |
| continue; | |
| } | |
| list($usage, $selector, $matching_type, $certificate) = $contentParts; | |
| // Prepare data payload | |
| $dnsData = [ | |
| 'type' => 'TLSA', | |
| 'name' => $stalwartRecord['name'], | |
| 'data' => [ | |
| 'usage' => $usage, | |
| 'selector' => $selector, | |
| 'matching_type' => $matching_type, | |
| 'certificate' => $certificate | |
| ], | |
| 'proxied' => false, | |
| 'ttl' => 120 | |
| ]; | |
| // Create record | |
| $addUrl = "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records"; | |
| $result = executeCurlRequest($addUrl, 'POST', ['Authorization: Bearer ' . $cfApiKey, 'Content-Type: application/json'], json_encode($dnsData)); | |
| if ($result['success']) { | |
| echo "Added: {$dnsData['name']} ({$dnsData['data']['certificate']})\n"; | |
| } else { | |
| echo "Add failed: {$dnsData['name']} - " . $result['errors'][0]['message'] . "\n"; | |
| } | |
| } | |
| echo "\nTLSA record synchronization completed.\n"; | |
| // Helper function to execute cURL requests | |
| function executeCurlRequest($url, $method = 'GET', $headers = [], $postFields = null) { | |
| $ch = curl_init($url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); | |
| curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
| if (in_array($method, ['POST', 'PUT', 'PATCH']) && $postFields !== null) { | |
| curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); | |
| } | |
| $response = curl_exec($ch); | |
| $error = curl_error($ch); | |
| curl_close($ch); | |
| if ($error) { | |
| throw new Exception("cURL Error: $error"); | |
| } | |
| return json_decode($response, true); | |
| } | |
| // Helper function to normalize records | |
| function normalizeRecords($records) { | |
| $normalized = []; | |
| foreach ($records as $record) { | |
| $cleanName = rtrim(strtolower(trim($record['name'])), '.'); | |
| $contentParts = explode(' ', trim($record['content'])); | |
| $contentParts = array_map('trim', $contentParts); | |
| $contentParts = array_slice($contentParts, 0, 4); | |
| $normalizedContent = implode(' ', $contentParts); | |
| $normalized[] = [ | |
| 'name' => $cleanName, | |
| 'content' => $normalizedContent | |
| ]; | |
| } | |
| return $normalized; | |
| } | |
| // Helper function to compare normalized records | |
| function compareRecords($stalwart, $cloudflare) { | |
| // Build arrays of keys for comparison | |
| $stalwartKeys = []; | |
| foreach ($stalwart as $record) { | |
| $stalwartKeys[$record['name']][$record['content']] = true; | |
| } | |
| $cfKeys = []; | |
| foreach ($cloudflare as $record) { | |
| $cfKeys[$record['name']][$record['content']] = true; | |
| } | |
| // Check if both arrays have the same keys and values | |
| return $stalwartKeys == $cfKeys; | |
| } | |
| ?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment