using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using AMWD.Net.Api.Cloudflare.Zones.Internals.Requests; namespace AMWD.Net.Api.Cloudflare.Zones { /// /// Extends the with methods for working with zones. /// public static class DnsRecordsExtensions { private static readonly IReadOnlyCollection _dataComponentTypes = [ DnsRecordType.Caa, DnsRecordType.Cert, DnsRecordType.DnsKey, DnsRecordType.Ds, DnsRecordType.Https, DnsRecordType.Loc, DnsRecordType.NaPtr, DnsRecordType.SMimeA, DnsRecordType.Srv, DnsRecordType.SshFp, DnsRecordType.SvcB, DnsRecordType.TlsA, DnsRecordType.Uri, ]; private static readonly IReadOnlyCollection _priorityTypes = [ DnsRecordType.Mx, DnsRecordType.Srv, DnsRecordType.Uri, ]; /// /// List, search, sort, and filter a zones' DNS records. /// /// The . /// The zone ID. /// Filter options (optional). /// A cancellation token used to propagate notification that this operation should be canceled. public static Task>> ListDnsRecords(this ICloudflareClient client, string zoneId, ListDnsRecordsFilter? options = null, CancellationToken cancellationToken = default) { zoneId.ValidateCloudflareId(); return client.GetAsync>($"zones/{zoneId}/dns_records", options, cancellationToken); } /// /// Create a new DNS record for a zone. /// /// /// /// A/AAAA records cannot exist on the same name as CNAME records. /// NS records cannot exist on the same name as any other record type. /// Domain names are always represented in Punycode, even if Unicode characters were used when creating the record. /// /// /// The . /// The request information. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> CreateDnsRecord(this ICloudflareClient client, CreateDnsRecordRequest request, CancellationToken cancellationToken = default) { request.ZoneId.ValidateCloudflareId(); var req = ValidateDnsRecordRequest(request); return client.PostAsync($"zones/{request.ZoneId}/dns_records", req, cancellationToken: cancellationToken); } /// /// Send a Batch of DNS Record API calls to be executed together. /// /// /// /// /// Although Cloudflare will execute the batched operations in a single database transaction, /// Cloudflare's distributed KV store must treat each record change as a single key-value pair. /// This means that the propagation of changes is not atomic. /// See the documentation for more information. /// /// /// The operations you specify within the batch request body are always executed in the following order /// /// Deletes (delete) /// Updates (patch) /// Overwrites (put) /// Creates (post) /// /// /// /// /// The . /// The request information. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> BatchDnsRecords(this ICloudflareClient client, BatchDnsRecordsRequest request, CancellationToken cancellationToken = default) { request.ZoneId.ValidateCloudflareId(); // Validate Deletes var deletes = new List(); foreach (string recordId in request.DnsRecordIdsToDelete) { recordId.ValidateCloudflareId(); deletes.Add(new InternalDnsRecordId { Id = recordId }); } // Validate Updates var updates = new List(); foreach (var update in request.DnsRecordsToUpdate) { update.Id.ValidateCloudflareId(); if (update.ZoneId != request.ZoneId) throw new ArgumentException($"The ZoneId of the update request ({update.ZoneId}) does not match the ZoneId of the batch request ({request.ZoneId})."); var baseReq = ValidateDnsRecordRequest(update); updates.Add(new InternalBatchUpdateRequest(update.Id, baseReq)); } // Validate Overwrites var overwrites = new List(); foreach (var overwrite in request.DnsRecordsToOverwrite) { overwrite.Id.ValidateCloudflareId(); if (overwrite.ZoneId != request.ZoneId) throw new ArgumentException($"The ZoneId of the overwrite request ({overwrite.ZoneId}) does not match the ZoneId of the batch request ({request.ZoneId})."); var baseReq = ValidateDnsRecordRequest(overwrite); overwrites.Add(new InternalBatchUpdateRequest(overwrite.Id, baseReq)); } // Validate Creates var creates = new List(); foreach (var create in request.DnsRecordsToCreate) creates.Add(ValidateDnsRecordRequest(create)); var req = new InternalBatchRequest(); if (deletes.Count > 0) req.Deletes = deletes; if (updates.Count > 0) req.Patches = updates; if (overwrites.Count > 0) req.Puts = overwrites; if (creates.Count > 0) req.Posts = creates; return client.PostAsync($"zones/{request.ZoneId}/dns_records/batch", req, cancellationToken: cancellationToken); } /// /// You can export your BIND config through this endpoint. /// /// /// See the documentation for more information. /// /// The . /// The zone ID. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> ExportDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) { zoneId.ValidateCloudflareId(); return client.GetAsync($"zones/{zoneId}/dns_records/export", cancellationToken: cancellationToken); } /// /// You can upload your BIND config through this endpoint. /// /// /// See the documentation for more information. /// /// The . /// The request information. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> ImportDnsRecords(this ICloudflareClient client, ImportDnsRecordsRequest request, CancellationToken cancellationToken = default) { request.ZoneId.ValidateCloudflareId(); if (string.IsNullOrWhiteSpace(request.File)) throw new ArgumentNullException(nameof(request.File)); var req = new MultipartFormDataContent(); if (request.Proxied.HasValue) req.Add(new StringContent(request.Proxied.Value.ToString().ToLowerInvariant()), "proxied"); byte[] content = File.Exists(request.File) ? File.ReadAllBytes(request.File) : Encoding.UTF8.GetBytes(request.File); req.Add(new ByteArrayContent(content), "file"); return client.PostAsync($"zones/{request.ZoneId}/dns_records/import", req, cancellationToken: cancellationToken); } /// /// Scan for common DNS records on your domain and automatically add them to your zone. ///
/// Useful if you haven't updated your nameservers yet. ///
/// The . /// The zone ID. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> ScanDnsRecords(this ICloudflareClient client, string zoneId, CancellationToken cancellationToken = default) { zoneId.ValidateCloudflareId(); return client.PostAsync($"zones/{zoneId}/dns_records/scan", null, cancellationToken: cancellationToken); } /// /// Deletes a DNS record. /// /// The . /// The zone ID. /// The record ID. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> DeleteDnsRecord(this ICloudflareClient client, string zoneId, string dnsRecordId, CancellationToken cancellationToken = default) { zoneId.ValidateCloudflareId(); dnsRecordId.ValidateCloudflareId(); return client.DeleteAsync($"zones/{zoneId}/dns_records/{dnsRecordId}", cancellationToken: cancellationToken); } /// /// Returns details for a DNS record. /// /// The . /// The zone ID. /// The record ID. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> DnsRecordDetails(this ICloudflareClient client, string zoneId, string dnsRecordId, CancellationToken cancellationToken = default) { zoneId.ValidateCloudflareId(); dnsRecordId.ValidateCloudflareId(); return client.GetAsync($"zones/{zoneId}/dns_records/{dnsRecordId}", cancellationToken: cancellationToken); } /// /// Update an existing DNS record. /// /// /// /// A/AAAA records cannot exist on the same name as CNAME records. /// NS records cannot exist on the same name as any other record type. /// Domain names are always represented in Punycode, even if Unicode characters were used when creating the record. /// /// /// The . /// The request information. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> UpdateDnsRecord(this ICloudflareClient client, UpdateDnsRecordRequest request, CancellationToken cancellationToken = default) { request.ZoneId.ValidateCloudflareId(); request.Id.ValidateCloudflareId(); var req = ValidateDnsRecordRequest(request); return client.PatchAsync($"zones/{request.ZoneId}/dns_records/{request.Id}", req, cancellationToken: cancellationToken); } /// /// Overwrite an existing DNS record. /// /// /// /// A/AAAA records cannot exist on the same name as CNAME records. /// NS records cannot exist on the same name as any other record type. /// Domain names are always represented in Punycode, even if Unicode characters were used when creating the record. /// /// /// The . /// The request information. /// A cancellation token used to propagate notification that this operation should be canceled. public static Task> OverwriteDnsRecord(this ICloudflareClient client, OverwriteDnsRecordRequest request, CancellationToken cancellationToken = default) { request.ZoneId.ValidateCloudflareId(); request.Id.ValidateCloudflareId(); var req = ValidateDnsRecordRequest(request); return client.PutAsync($"zones/{request.ZoneId}/dns_records/{request.Id}", req, cancellationToken: cancellationToken); } private static InternalDnsRecordRequest ValidateDnsRecordRequest(CreateDnsRecordRequest request) { if (string.IsNullOrWhiteSpace(request.Name)) throw new ArgumentNullException(nameof(request.Name)); if (!Enum.IsDefined(typeof(DnsRecordType), request.Type)) throw new ArgumentOutOfRangeException(nameof(request.Type), request.Type, "Value must be one of the 'ZoneDnsRecordType' enum values."); var req = new InternalDnsRecordRequest(request.Type, request.Name) { Comment = request.Comment?.Trim(), Proxied = request.Proxied, Tags = request.Tags?.Where(t => !string.IsNullOrWhiteSpace(t)).ToList() }; if (!_dataComponentTypes.Contains(request.Type) && string.IsNullOrWhiteSpace(request.Content)) throw new ArgumentNullException(nameof(request.Content)); else req.Content = request.Content?.Trim(); if (request.Type == DnsRecordType.Cname && request.Settings != null && request.Settings is DnsRecord.CnameSettings cnameSettings) req.Settings = new DnsRecord.CnameSettings(cnameSettings.FlattenCname); if (_dataComponentTypes.Contains(request.Type) && request.Data == null) throw new ArgumentNullException(nameof(request.Data)); if (_priorityTypes.Contains(request.Type)) { if (!request.Priority.HasValue) throw new ArgumentNullException(nameof(request.Priority)); req.Priority = request.Priority.Value; } if (request.Ttl.HasValue) { if (request.Ttl == 1) { req.Ttl = 1; } else if (request.Ttl < 30 || 86400 < request.Ttl) { throw new ArgumentOutOfRangeException(nameof(request.Ttl), request.Ttl, "Value must be between 60 (Enterprise: 30) and 86400."); } else { req.Ttl = request.Ttl; } } switch (request.Type) { case DnsRecordType.Caa: if (request.Data is DnsRecord.CaaData caaData) { if (string.IsNullOrWhiteSpace(caaData.Tag)) throw new ArgumentNullException(nameof(caaData.Tag)); if (string.IsNullOrWhiteSpace(caaData.Value)) throw new ArgumentNullException(nameof(caaData.Value)); req.Data = new DnsRecord.CaaData(caaData.Flags, caaData.Tag, caaData.Value); } else { throw new ArgumentException("Value must be of type 'CaaData'.", nameof(request.Data)); } break; case DnsRecordType.Cert: if (request.Data is DnsRecord.CertData certData) { if (string.IsNullOrWhiteSpace(certData.Certificate)) throw new ArgumentNullException(nameof(certData.Certificate)); req.Data = new DnsRecord.CertData(certData.Algorithm, certData.Certificate, certData.KeyTag, certData.Type); } else { throw new ArgumentException("Value must be of type 'CertData'.", nameof(request.Data)); } break; case DnsRecordType.DnsKey: if (request.Data is DnsRecord.DnsKeyData dnsKeyData) { if (string.IsNullOrWhiteSpace(dnsKeyData.PublicKey)) throw new ArgumentNullException(nameof(dnsKeyData.PublicKey)); req.Data = new DnsRecord.DnsKeyData(dnsKeyData.Algorithm, dnsKeyData.Flags, dnsKeyData.Protocol, dnsKeyData.PublicKey); } else { throw new ArgumentException("Value must be of type 'DnsKeyData'.", nameof(request.Data)); } break; case DnsRecordType.Ds: if (request.Data is DnsRecord.DsData dsData) { if (string.IsNullOrWhiteSpace(dsData.Digest)) throw new ArgumentNullException(nameof(dsData.Digest)); req.Data = new DnsRecord.DsData(dsData.Algorithm, dsData.Digest, dsData.DigestType, dsData.KeyTag); } else { throw new ArgumentException("Value must be of type 'DsData'.", nameof(request.Data)); } break; case DnsRecordType.Https: if (request.Data is DnsRecord.HttpsData httpsData) { if (string.IsNullOrWhiteSpace(httpsData.Target)) throw new ArgumentNullException(nameof(httpsData.Target)); if (string.IsNullOrWhiteSpace(httpsData.Value)) throw new ArgumentNullException(nameof(httpsData.Value)); req.Data = new DnsRecord.HttpsData(httpsData.Priority, httpsData.Target, httpsData.Value); } else { throw new ArgumentException("Value must be of type 'HttpsData'.", nameof(request.Data)); } break; case DnsRecordType.Loc: if (request.Data is DnsRecord.LocData locData) { if (locData.LatitudeDegrees < 0 || 90 < locData.LatitudeDegrees) throw new ArgumentOutOfRangeException(nameof(locData.LatitudeDegrees), locData.LatitudeDegrees, "Value must be between 0 and 90."); if (locData.LatitudeMinutes < 0 || 59 < locData.LatitudeMinutes) throw new ArgumentOutOfRangeException(nameof(locData.LatitudeMinutes), locData.LatitudeMinutes, "Value must be between 0 and 59."); if (locData.LatitudeSeconds < 0 || 59.999 < locData.LatitudeSeconds) throw new ArgumentOutOfRangeException(nameof(locData.LatitudeSeconds), locData.LatitudeSeconds, "Value must be between 0 and 59.999."); if (!Enum.IsDefined(typeof(DnsRecord.LatitudeDirection), locData.LatitudeDirection)) throw new ArgumentOutOfRangeException(nameof(locData.LatitudeDirection), locData.LatitudeDirection, "Value must be one of the 'ZoneDnsRecord.LatitudeDirection' enum values."); if (locData.LongitudeDegrees < 0 || 180 < locData.LongitudeDegrees) throw new ArgumentOutOfRangeException(nameof(locData.LongitudeDegrees), locData.LongitudeDegrees, "Value must be between 0 and 180."); if (locData.LongitudeMinutes < 0 || 59 < locData.LongitudeMinutes) throw new ArgumentOutOfRangeException(nameof(locData.LongitudeMinutes), locData.LongitudeMinutes, "Value must be between 0 and 59."); if (locData.LongitudeSeconds < 0 || 59.999 < locData.LongitudeSeconds) throw new ArgumentOutOfRangeException(nameof(locData.LongitudeSeconds), locData.LongitudeSeconds, "Value must be between 0 and 59.999."); if (!Enum.IsDefined(typeof(DnsRecord.LongitudeDirection), locData.LongitudeDirection)) throw new ArgumentOutOfRangeException(nameof(locData.LongitudeDirection), locData.LongitudeDirection, "Value must be one of the 'ZoneDnsRecord.LongitudeDirection' enum values."); if (locData.Altitude < -100_000 || 42_849_672.95 < locData.Altitude) throw new ArgumentOutOfRangeException(nameof(locData.Altitude), locData.Altitude, "Value must be between -100,000.00 and 42,849,672.95."); if (locData.Size < 0 || 90_000_000 < locData.Size) throw new ArgumentOutOfRangeException(nameof(locData.Size), locData.Size, "Value must be between 0 and 90,000,000."); if (locData.PrecisionHorizontal < 0 || 90_000_000 < locData.PrecisionHorizontal) throw new ArgumentOutOfRangeException(nameof(locData.PrecisionHorizontal), locData.PrecisionHorizontal, "Value must be between 0 and 90,000,000."); if (locData.PrecisionVertical < 0 || 90_000_000 < locData.PrecisionVertical) throw new ArgumentOutOfRangeException(nameof(locData.PrecisionVertical), locData.PrecisionVertical, "Value must be between 0 and 90,000,000."); req.Data = new DnsRecord.LocData( latitudeDegrees: locData.LatitudeDegrees, latitudeMinutes: locData.LatitudeMinutes, latitudeSeconds: Math.Floor(locData.LatitudeSeconds * 1000) / 1000, // Truncate to 3 decimal places latitudeDirection: locData.LatitudeDirection, longitudeDegrees: locData.LongitudeDegrees, longitudeMinutes: locData.LongitudeMinutes, longitudeSeconds: Math.Floor(locData.LongitudeSeconds * 1000) / 1000, // Truncate to 3 decimal places longitudeDirection: locData.LongitudeDirection, altitude: Math.Floor(locData.Altitude * 100) / 100, // Truncate to 2 decimal places size: locData.Size, precisionHorizontal: locData.PrecisionHorizontal, precisionVertical: locData.PrecisionVertical ); } else { throw new ArgumentException("Value must be of type 'LocData'.", nameof(request.Data)); } break; case DnsRecordType.NaPtr: if (request.Data is DnsRecord.NaPtrData naPtrData) { if (string.IsNullOrWhiteSpace(naPtrData.Flags)) throw new ArgumentNullException(nameof(naPtrData.Flags)); if (string.IsNullOrWhiteSpace(naPtrData.Regex)) throw new ArgumentNullException(nameof(naPtrData.Regex)); if (string.IsNullOrWhiteSpace(naPtrData.Replacement)) throw new ArgumentNullException(nameof(naPtrData.Replacement)); if (string.IsNullOrWhiteSpace(naPtrData.Service)) throw new ArgumentNullException(nameof(naPtrData.Service)); req.Data = new DnsRecord.NaPtrData( naPtrData.Flags, naPtrData.Order, naPtrData.Preference, naPtrData.Regex, naPtrData.Replacement, naPtrData.Service ); } else { throw new ArgumentException("Value must be of type 'NaPtrData'.", nameof(request.Data)); } break; case DnsRecordType.SMimeA: if (request.Data is DnsRecord.SMimeAData sMimeAData) { if (string.IsNullOrWhiteSpace(sMimeAData.Certificate)) throw new ArgumentNullException(nameof(sMimeAData.Certificate)); req.Data = new DnsRecord.SMimeAData(sMimeAData.Certificate, sMimeAData.MatchingType, sMimeAData.Selector, sMimeAData.Usage); } else { throw new ArgumentException("Value must be of type 'SMimeAData'.", nameof(request.Data)); } break; case DnsRecordType.Srv: if (request.Data is DnsRecord.SrvData srvData) { if (string.IsNullOrWhiteSpace(srvData.Target)) throw new ArgumentNullException(nameof(srvData.Target)); req.Data = new DnsRecord.SrvData(srvData.Port, srvData.Priority, srvData.Target, srvData.Weight); } else { throw new ArgumentException("Value must be of type 'SrvData'.", nameof(request.Data)); } break; case DnsRecordType.SshFp: if (request.Data is DnsRecord.SshFpData sshFpData) { if (string.IsNullOrWhiteSpace(sshFpData.Fingerprint)) throw new ArgumentNullException(nameof(sshFpData.Fingerprint)); req.Data = new DnsRecord.SshFpData(sshFpData.Algorithm, sshFpData.Fingerprint, sshFpData.Type); } else { throw new ArgumentException("Value must be of type 'SshFpData'.", nameof(request.Data)); } break; case DnsRecordType.SvcB: if (request.Data is DnsRecord.SvcBData svcBData) { if (string.IsNullOrWhiteSpace(svcBData.Target)) throw new ArgumentNullException(nameof(svcBData.Target)); if (string.IsNullOrWhiteSpace(svcBData.Value)) throw new ArgumentNullException(nameof(svcBData.Value)); req.Data = new DnsRecord.SvcBData(svcBData.Priority, svcBData.Target, svcBData.Value); } else { throw new ArgumentException("Value must be of type 'SvcBData'.", nameof(request.Data)); } break; case DnsRecordType.TlsA: if (request.Data is DnsRecord.TlsAData tlsAData) { if (string.IsNullOrWhiteSpace(tlsAData.Certificate)) throw new ArgumentNullException(nameof(tlsAData.Certificate)); req.Data = new DnsRecord.TlsAData(tlsAData.Certificate, tlsAData.MatchingType, tlsAData.Selector, tlsAData.Usage); } else { throw new ArgumentException("Value must be of type 'TlsAData'.", nameof(request.Data)); } break; case DnsRecordType.Uri: if (request.Data is DnsRecord.UriData uriData) { if (string.IsNullOrWhiteSpace(uriData.Target)) throw new ArgumentNullException(nameof(uriData.Target)); req.Data = new DnsRecord.UriData(uriData.Target, uriData.Weight); } else { throw new ArgumentException("Value must be of type 'UriData'.", nameof(request.Data)); } break; } return req; } } }