diff --git a/misc/Ae.Dns.Console/Program.cs b/misc/Ae.Dns.Console/Program.cs index a76733f..17d2539 100644 --- a/misc/Ae.Dns.Console/Program.cs +++ b/misc/Ae.Dns.Console/Program.cs @@ -1,10 +1,10 @@ using Ae.Dns.Client; using Ae.Dns.Client.Filters; using Ae.Dns.Client.Lookup; -using Ae.Dns.Client.Zone; using Ae.Dns.Metrics.InfluxDb; using Ae.Dns.Protocol; using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Zone; using Ae.Dns.Server; using Ae.Dns.Server.Http; using App.Metrics; @@ -211,7 +211,11 @@ async Task ReportStats(CancellationToken token) if (dnsConfiguration.UpdateZoneName != null && dnsConfiguration.UpdateZoneFile != null) { #pragma warning disable CS0618 // Type or member is obsolete - updateClient = new DnsUpdateClient(new FileDnsZone(dnsConfiguration.UpdateZoneName, new FileInfo(dnsConfiguration.UpdateZoneFile))); + updateClient = new DnsUpdateClient(new DnsZone + { + Origin = dnsConfiguration.UpdateZoneName, + DefaultTtl = TimeSpan.FromHours(1) + }); #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Ae.Dns.Client/DnsUpdateClient.cs b/src/Ae.Dns.Client/DnsUpdateClient.cs index a05ac6c..456e150 100644 --- a/src/Ae.Dns.Client/DnsUpdateClient.cs +++ b/src/Ae.Dns.Client/DnsUpdateClient.cs @@ -1,7 +1,7 @@ -using Ae.Dns.Client.Zone; -using Ae.Dns.Protocol; +using Ae.Dns.Protocol; using Ae.Dns.Protocol.Enums; using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Zone; using System; using System.Collections.Generic; using System.Linq; @@ -28,26 +28,36 @@ public DnsUpdateClient(IDnsZone dnsZone) } /// - public async Task Query(DnsMessage query, CancellationToken token = default) + public Task Query(DnsMessage query, CancellationToken token = default) { query.EnsureOperationCode(DnsOperationCode.UPDATE); var hostnames = query.Nameservers.Select(x => x.Host).ToArray(); - void RemoveStaleRecords(ICollection records) + void ChangeRecords(ICollection records) { foreach (var recordToRemove in records.Where(x => hostnames.Contains(x.Host)).ToArray()) { records.Remove(recordToRemove); } + + foreach (var nameserver in query.Nameservers) + { + records.Add(nameserver); + } }; - if (query.Nameservers.Count > 0 && hostnames.Any(x => x.Last() != _dnsZone.Name) && await _dnsZone.ChangeRecords(RemoveStaleRecords, query.Nameservers, token)) + if (query.Nameservers.Count > 0 && hostnames.Any(x => x.Last() != _dnsZone.Origin)) { - return query.CreateAnswerMessage(DnsResponseCode.NoError, ToString()); + lock (_dnsZone) + { + ChangeRecords(_dnsZone.Records); + } + + return Task.FromResult(query.CreateAnswerMessage(DnsResponseCode.NoError, ToString())); } - return query.CreateAnswerMessage(DnsResponseCode.Refused, ToString()); + return Task.FromResult(query.CreateAnswerMessage(DnsResponseCode.Refused, ToString())); } /// diff --git a/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs b/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs index 37f6d26..fe29703 100644 --- a/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs +++ b/src/Ae.Dns.Client/Lookup/DnsZoneLookup.cs @@ -1,5 +1,5 @@ -using Ae.Dns.Client.Zone; -using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Zone; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Ae.Dns.Client/Zone/FileDnsZone.cs b/src/Ae.Dns.Client/Zone/FileDnsZone.cs deleted file mode 100644 index 377a9c8..0000000 --- a/src/Ae.Dns.Client/Zone/FileDnsZone.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Ae.Dns.Protocol.Records; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Ae.Dns.Client.Zone -{ - /// - /// Represents a file-backed DNS zone. - /// - [Obsolete("Experimental: May change significantly in the future")] - public sealed class FileDnsZone : IDnsZone - { - /// - /// Construct a new zone, using the specified name (suffix) and file for persistence. - /// - /// - /// - public FileDnsZone(string name, FileInfo file) - { - _name = name; - _file = file; - _ = ReloadZone(CancellationToken.None); - } - - private readonly SemaphoreSlim _zoneLock = new SemaphoreSlim(1, 1); - private readonly IList _records = new List(); - private readonly string _name; - private readonly FileInfo _file; - - /// - public IEnumerable Records => _records; - - /// - public string Name => _name; - - private async Task ReloadZone(CancellationToken token) - { - await _zoneLock.WaitAsync(token); - - try - { - DeserializeZone(); - } - finally - { - _zoneLock.Release(); - } - } - - /// - public async Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default) - { - await _zoneLock.WaitAsync(token); - - try - { - changeDelegate(_records); - - foreach (var recordToAdd in recordsToAdd) - { - _records.Add(recordToAdd); - } - - SerializeZone(); - } - finally - { - _zoneLock.Release(); - } - - return true; - } - - private void DeserializeZone() - { - using var file = _file.Open(FileMode.OpenOrCreate, FileAccess.Read); - using var reader = new BinaryReader(file); - - _records.Clear(); - - for (int i = 0; i < reader.ReadInt32(); i++) - { - var length = reader.ReadInt32(); - var buffer = reader.ReadBytes(length); - - var record = new DnsResourceRecord(); - - int offset = 0; - record.ReadBytes(buffer, ref offset); - - _records.Add(record); - } - } - - private void SerializeZone() - { - using var file = _file.Open(FileMode.OpenOrCreate, FileAccess.Write); - using var writer = new BinaryWriter(file); - - writer.Write(_records.Count); - - foreach (var record in _records) - { - var buffer = new byte[4096]; - - int offset = 0; - record.WriteBytes(buffer, ref offset); - - writer.Write(offset); - writer.Write(buffer, 0, offset); - } - } - } -} diff --git a/src/Ae.Dns.Client/Zone/IDnsZone.cs b/src/Ae.Dns.Client/Zone/IDnsZone.cs deleted file mode 100644 index f33219c..0000000 --- a/src/Ae.Dns.Client/Zone/IDnsZone.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Ae.Dns.Protocol.Records; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Ae.Dns.Client.Zone -{ - /// - /// Provides methods to store data against a DNS zone. - /// - [Obsolete("Experimental: May change significantly in the future")] - public interface IDnsZone - { - /// - /// Add the specified enumerable of to this zone. - /// - /// - /// - /// - Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default); - - /// - /// Get all records in the zone. - /// - IEnumerable Records { get; } - - /// - /// The name of the zone. - /// - string Name { get; } - } -} diff --git a/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs b/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs index e01cf65..9ccfbfd 100644 --- a/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsDomainResource.cs @@ -1,4 +1,6 @@ -namespace Ae.Dns.Protocol.Records +using Ae.Dns.Protocol.Zone; + +namespace Ae.Dns.Protocol.Records { /// /// Represents a DNS text resource containing a domain name. @@ -15,5 +17,17 @@ public sealed class DnsDomainResource : DnsStringResource /// public override string ToString() => Domain; + + /// + public override string ToZone(IDnsZone zone) + { + return zone.ToFormattedHost(Entries); + } + + /// + public override void FromZone(IDnsZone zone, string input) + { + Entries = zone.FromFormattedHost(input); + } } } diff --git a/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs b/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs index 06173e1..71a95ba 100644 --- a/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsIpAddressResource.cs @@ -1,4 +1,5 @@ using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Zone; using System; using System.Collections.Generic; using System.Net; @@ -60,5 +61,18 @@ public void WriteBytes(Memory bytes, ref int offset) address.CopyTo(bytes.Slice(offset).Span); offset += address.Length; } + + + /// + public string ToZone(IDnsZone zone) + { + return IPAddress.ToString(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + IPAddress = IPAddress.Parse(input); + } } } diff --git a/src/Ae.Dns.Protocol/Records/DnsMxResource.cs b/src/Ae.Dns.Protocol/Records/DnsMxResource.cs index 450bedb..fcd3e3a 100644 --- a/src/Ae.Dns.Protocol/Records/DnsMxResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsMxResource.cs @@ -1,4 +1,5 @@ using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Zone; using System; namespace Ae.Dns.Protocol.Records @@ -53,7 +54,21 @@ public override void ReadBytes(ReadOnlyMemory bytes, ref int offset, int l } /// - public override string ToString() => Exchange; + public override void FromZone(IDnsZone zone, string input) + { + var parts = input.Split(null); + Preference = ushort.Parse(parts[0]); + Entries = zone.FromFormattedHost(parts[1]); + } + + /// + public override string ToZone(IDnsZone zone) + { + return $"{Preference} {zone.ToFormattedHost(Entries)}"; + } + + /// + public override string ToString() => $"{Preference} {Entries}"; /// public override void WriteBytes(Memory bytes, ref int offset) diff --git a/src/Ae.Dns.Protocol/Records/DnsOptResource.cs b/src/Ae.Dns.Protocol/Records/DnsOptResource.cs index c2e0686..9f3cd19 100644 --- a/src/Ae.Dns.Protocol/Records/DnsOptResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsOptResource.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; namespace Ae.Dns.Protocol.Records { /// @@ -46,6 +47,18 @@ public void WriteBytes(Memory bytes, ref int offset) offset += Raw.Length; } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() => $"Raw: {Raw.Length} bytes"; } diff --git a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs index bb821fa..5e795be 100644 --- a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs +++ b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs @@ -1,4 +1,5 @@ using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Zone; using System; using System.Linq; @@ -7,7 +8,7 @@ namespace Ae.Dns.Protocol.Records /// /// Represents metadata around a DNS resource record returned by a DNS server. /// - public sealed class DnsResourceRecord : IEquatable, IDnsByteArrayReader, IDnsByteArrayWriter + public sealed class DnsResourceRecord : IEquatable, IDnsByteArrayReader, IDnsByteArrayWriter, IDnsZoneConverter { /// /// The type of DNS query. @@ -118,5 +119,24 @@ public void WriteBytes(Memory bytes, ref int offset) // Advance the offset with the size of the resource offset += resourceSize; } + + /// + public string ToZone(IDnsZone zone) + { + return $"{zone.ToFormattedHost(Host)} {Class} {Type} {Resource.ToZone(zone)}"; + } + + /// + public void FromZone(IDnsZone zone, string input) + { + var parts = input.Split(null, 4); + + Host = zone.FromFormattedHost(parts[0]); + Class = (DnsQueryClass)Enum.Parse(typeof(DnsQueryClass), parts[1]); + Type = (DnsQueryType)Enum.Parse(typeof(DnsQueryType), parts[2]); + TimeToLive = (uint)zone.DefaultTtl.TotalSeconds; + Resource = CreateResourceRecord(Type); + Resource.FromZone(zone, parts[3]); + } } } diff --git a/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs b/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs index 11d9f2e..617585d 100644 --- a/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsServiceBindingResource.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Sockets; using Ae.Dns.Protocol.Records.ServiceBinding; +using Ae.Dns.Protocol.Zone; namespace Ae.Dns.Protocol.Records { @@ -102,6 +103,18 @@ public void WriteBytes(Memory bytes, ref int offset) } } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() { diff --git a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs index 056f784..df67a21 100644 --- a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Linq; namespace Ae.Dns.Protocol.Records @@ -89,7 +90,28 @@ public void ReadBytes(ReadOnlyMemory bytes, ref int offset, int length) } /// - public override string ToString() => MName; + public string ToZone(IDnsZone zone) + { + return string.Join(" ", new string[] { MName + '.', RName + '.', $"({Serial} {(int)Refresh.TotalSeconds} {(int)Retry.TotalSeconds} {(int)Expire.TotalSeconds} {(int)Minimum.TotalSeconds})" }); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + var parts = input.Split(null, 3); + MName = parts[0].Trim('.'); + RName = parts[1].Trim('.'); + + var parts1 = parts[2].Trim(new[] { '(', ')' }).Split(null); + Serial = uint.Parse(parts1[0]); + Refresh = TimeSpan.FromSeconds(int.Parse(parts1[1])); + Retry = TimeSpan.FromSeconds(int.Parse(parts1[2])); + Expire = TimeSpan.FromSeconds(int.Parse(parts1[3])); + Minimum = TimeSpan.FromSeconds(uint.Parse(parts1[4])); + } + + /// + public override string ToString() => string.Join(" ", new string[] { MName, RName, $"({Serial} {(int)Refresh.TotalSeconds} {(int)Retry.TotalSeconds} {(int)Expire.TotalSeconds} {(int)Minimum.TotalSeconds})" }); /// public void WriteBytes(Memory bytes, ref int offset) diff --git a/src/Ae.Dns.Protocol/Records/DnsStringResource.cs b/src/Ae.Dns.Protocol/Records/DnsStringResource.cs index c76cd22..30f942d 100644 --- a/src/Ae.Dns.Protocol/Records/DnsStringResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsStringResource.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Linq; namespace Ae.Dns.Protocol.Records @@ -51,5 +52,11 @@ public virtual void WriteBytes(Memory bytes, ref int offset) { DnsByteExtensions.ToBytes(Entries.ToArray(), bytes, ref offset); } + + /// + public abstract string ToZone(IDnsZone zone); + + /// + public abstract void FromZone(IDnsZone zone, string input); } } diff --git a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs index 1200581..a3ede3e 100644 --- a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs @@ -1,4 +1,4 @@ -using System.Linq; +using Ae.Dns.Protocol.Zone; namespace Ae.Dns.Protocol.Records { @@ -11,6 +11,18 @@ public sealed class DnsTextResource : DnsStringResource protected override bool CanUseCompression => false; /// - public override string ToString() => '"' + string.Join("\", \"", Entries.ToArray()) + '"'; + public override string ToZone(IDnsZone zone) + { + throw new System.NotImplementedException(); + } + + /// + public override void FromZone(IDnsZone zone, string input) + { + throw new System.NotImplementedException(); + } + + /// + public override string ToString() => Entries; } } diff --git a/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs b/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs index cfc51c6..446694f 100644 --- a/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsUnknownResource.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Linq; namespace Ae.Dns.Protocol.Records @@ -40,6 +41,18 @@ public void ReadBytes(ReadOnlyMemory bytes, ref int offset, int length) Raw = DnsByteExtensions.ReadBytes(bytes, length, ref offset); } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() => $"Raw {Raw.Length} bytes"; diff --git a/src/Ae.Dns.Protocol/Records/IDnsResource.cs b/src/Ae.Dns.Protocol/Records/IDnsResource.cs index baff1fb..d1e2a6e 100644 --- a/src/Ae.Dns.Protocol/Records/IDnsResource.cs +++ b/src/Ae.Dns.Protocol/Records/IDnsResource.cs @@ -5,7 +5,7 @@ namespace Ae.Dns.Protocol.Records /// /// Represents a type of DNS resource. /// - public interface IDnsResource : IDnsByteArrayWriter + public interface IDnsResource : IDnsByteArrayWriter, IDnsZoneConverter { /// /// Read from the specified byte array, starting at the specified offset. diff --git a/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs b/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs new file mode 100644 index 0000000..884dd65 --- /dev/null +++ b/src/Ae.Dns.Protocol/Records/IDnsZoneConverter.cs @@ -0,0 +1,22 @@ +using Ae.Dns.Protocol.Zone; + +namespace Ae.Dns.Protocol.Records +{ + /// + /// Provides methods to convert to and from the DNS zone file format. + /// + public interface IDnsZoneConverter + { + /// + /// Write to a zone file string. + /// + /// + public string ToZone(IDnsZone zone); + /// + /// Read from a zone file string. + /// + /// + /// + public void FromZone(IDnsZone zone, string input); + } +} diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs index 8d15282..6ea65f0 100644 --- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs +++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcIpAddressesParameter.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Linq; using System.Net; using System.Net.Sockets; @@ -74,6 +75,18 @@ public void WriteBytes(Memory bytes, ref int offset) } } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() => string.Join(",", Entries.Select(x => x.ToString())); } diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs index f46520c..6305740 100644 --- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs +++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcStringParameter.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Collections.Generic; using System.Linq; @@ -49,6 +50,18 @@ public void WriteBytes(Memory bytes, ref int offset) } } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() => string.Join(",", (IEnumerable)Entries); } diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs index aee055f..61d6a09 100644 --- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs +++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUShortParameter.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; namespace Ae.Dns.Protocol.Records.ServiceBinding { @@ -35,6 +36,18 @@ public bool Equals(SvcUShortParameter? other) /// public void WriteBytes(Memory bytes, ref int offset) => DnsByteExtensions.ToBytes(Value, bytes, ref offset); + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string ToString() => Value.ToString(); } diff --git a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs index fb2eb59..929a11c 100644 --- a/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs +++ b/src/Ae.Dns.Protocol/Records/ServiceBinding/SvcUtf8StringParameter.cs @@ -1,4 +1,5 @@ -using System; +using Ae.Dns.Protocol.Zone; +using System; using System.Text; namespace Ae.Dns.Protocol.Records.ServiceBinding @@ -45,6 +46,18 @@ public void WriteBytes(Memory bytes, ref int offset) offset += stringBytes.Length; } + /// + public string ToZone(IDnsZone zone) + { + throw new NotImplementedException(); + } + + /// + public void FromZone(IDnsZone zone, string input) + { + throw new NotImplementedException(); + } + /// public override string? ToString() => Value; } diff --git a/src/Ae.Dns.Protocol/Zone/DnsZone.cs b/src/Ae.Dns.Protocol/Zone/DnsZone.cs new file mode 100644 index 0000000..5a4efb6 --- /dev/null +++ b/src/Ae.Dns.Protocol/Zone/DnsZone.cs @@ -0,0 +1,106 @@ +using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Records; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Ae.Dns.Protocol.Zone +{ + /// + /// Represents a file-backed DNS zone. + /// + [Obsolete("Experimental: May change significantly in the future")] + public sealed class DnsZone : IDnsZone + { + /// + public IList Records { get; set; } = new List(); + + /// + public DnsLabels Origin { get; set; } + + /// + public TimeSpan DefaultTtl { get; set; } + + /// + public void SerializeZone(StreamWriter writer) + { + writer.WriteLine($"$ORIGIN {Origin}."); + writer.WriteLine($"$TTL {(int)DefaultTtl.TotalSeconds}"); + + // Copy the records + var records = Records.ToList(); + + // Singular SOA must come first + var soa = records.Single(x => x.Type == DnsQueryType.SOA); + + records.Remove(soa); + records.Insert(0, soa); + + foreach (var record in records) + { + writer.WriteLine(record.ToZone(this)); + } + } + + /// + public void DeserializeZone(StreamReader reader) + { + while (!reader.EndOfStream) + { + var line = reader.ReadLine()?.Trim() ?? string.Empty; + + if (line.StartsWith("$ORIGIN")) + { + Origin = line.Substring("$ORIGIN".Length).Trim().Trim('.'); + continue; + } + + if (line.StartsWith("$TTL")) + { + DefaultTtl = TimeSpan.FromSeconds(int.Parse(line.Substring("$TTL".Length))); + continue; + } + + var record = new DnsResourceRecord(); + record.FromZone(this, line); + Records.Add(record); + } + + } + + /// + public string FromFormattedHost(string host) + { + if (host == "@") + { + return Origin; + } + else if (host.EndsWith(".")) + { + return host.Substring(0, host.Length - 1); + } + else + { + return host + "." + Origin; + } + } + + /// + public string ToFormattedHost(string host) + { + if (host == Origin) + { + return "@"; + } + else if (host.EndsWith(Origin)) + { + return host.Substring(0, host.Length - Origin.ToString().Length - 1); + } + else + { + return host + '.'; + } + } + } +} diff --git a/src/Ae.Dns.Protocol/Zone/IDnsZone.cs b/src/Ae.Dns.Protocol/Zone/IDnsZone.cs new file mode 100644 index 0000000..bddcd32 --- /dev/null +++ b/src/Ae.Dns.Protocol/Zone/IDnsZone.cs @@ -0,0 +1,54 @@ +using Ae.Dns.Protocol.Records; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Ae.Dns.Protocol.Zone +{ + /// + /// Provides methods to store data against a DNS zone. + /// + public interface IDnsZone + { + /// + /// Get all records in the zone. + /// + IList Records { get; set; } + + /// + /// The name of the zone. + /// + DnsLabels Origin { get; set; } + + /// + /// The default TTL of the zone. + /// + TimeSpan DefaultTtl { get; set; } + + /// + /// Serialize the zone to a . + /// + /// + void SerializeZone(StreamWriter writer); + + /// + /// Deserialize the zone from a . + /// + /// + void DeserializeZone(StreamReader reader); + + /// + /// Format a host from the zone file format. + /// + /// + /// + string FromFormattedHost(string host); + + /// + /// Format a host name for a zone file. + /// + /// + /// + string ToFormattedHost(string host); + } +} diff --git a/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs b/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs index a2d5333..87c5e5d 100644 --- a/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs +++ b/tests/Ae.Dns.Tests/Client/Lookup/DnsZoneLookupTests.cs @@ -1,28 +1,49 @@ -using Ae.Dns.Client.Lookup; -using Ae.Dns.Client.Zone; +#pragma warning disable CS0618 // Type or member is obsolete + +using Ae.Dns.Client.Lookup; +using Ae.Dns.Protocol; using Ae.Dns.Protocol.Enums; using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Zone; using System; using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace Ae.Dns.Tests.Client.Lookup { public sealed class DnsZoneLookupTests { private sealed class DummyZoneNoRecords : IDnsZone { - public IEnumerable Records => Enumerable.Empty(); + public IList Records { get; set; } = new List(); + public DnsLabels Origin { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public TimeSpan DefaultTtl { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public string Name => throw new NotImplementedException(); + public Task ChangeRecords(Action> changeDelegate, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public void DeserializeZone(StreamReader reader) + { + throw new NotImplementedException(); + } + + public string FromFormattedHost(string host) + { + throw new NotImplementedException(); + } + + public void SerializeZone(StreamWriter writer) + { + throw new NotImplementedException(); + } - public Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default) + public string ToFormattedHost(string host) { throw new NotImplementedException(); } @@ -41,7 +62,7 @@ public void TestLookupNoResults() private sealed class DummyZoneWithRecords : IDnsZone { - public IEnumerable Records => new[] + public IList Records { get; set; } = new[] { new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.A, Resource = new DnsIpAddressResource { IPAddress = IPAddress.Loopback } }, new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.A, Resource = new DnsIpAddressResource { IPAddress = IPAddress.Broadcast } }, @@ -49,9 +70,30 @@ private sealed class DummyZoneWithRecords : IDnsZone new DnsResourceRecord { Host = "wibble", Class = DnsQueryClass.IN, Type = DnsQueryType.TEXT, Resource = new DnsTextResource { Entries = "hello2" } }, }; - public string Name => throw new NotImplementedException(); + public DnsLabels Origin { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public TimeSpan DefaultTtl { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public Task ChangeRecords(Action> changeDelegate, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public void DeserializeZone(StreamReader reader) + { + throw new NotImplementedException(); + } + + public string FromFormattedHost(string host) + { + throw new NotImplementedException(); + } + + public void SerializeZone(StreamWriter writer) + { + throw new NotImplementedException(); + } - public Task ChangeRecords(Action> changeDelegate, IEnumerable recordsToAdd, CancellationToken token = default) + public string ToFormattedHost(string host) { throw new NotImplementedException(); } diff --git a/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs new file mode 100644 index 0000000..2a98aba --- /dev/null +++ b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs @@ -0,0 +1,184 @@ +using Ae.Dns.Protocol.Enums; +using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Zone; +using System; +using System.IO; +using System.Net; +using System.Text; +using Xunit; + +namespace Ae.Dns.Tests.Zone +{ + [Obsolete] + public sealed class DnsZoneTests + { + [Fact] + public void TestRoundTripZone() + { + var originalZone = new DnsZone(); + + originalZone.Origin = "example.com"; + originalZone.DefaultTtl = TimeSpan.FromSeconds(3600); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.SOA, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsSoaResource + { + MName = "ns.example.com", + RName = "username.example.com", + Serial = 2020091025, + Refresh = TimeSpan.FromSeconds(7200), + Retry = TimeSpan.FromSeconds(3600), + Expire = TimeSpan.FromSeconds(1209600), + Minimum = TimeSpan.FromSeconds(3600) + } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.NS, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsDomainResource { Entries = "ns" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.NS, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsDomainResource { Entries = "ns.somewhere.example" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.MX, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsMxResource { Preference = 10, Entries = "mail.example.com" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.MX, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsMxResource { Preference = 20, Entries = "mail2.example.com" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.MX, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsMxResource { Preference = 50, Entries = "mail3" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.A, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.1") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.AAAA, + Host = "example.com", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("2001:db8:10::1") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.A, + Host = "ns", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.2") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.AAAA, + Host = "ns", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("2001:db8:10::2") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.CNAME, + Host = "www", + TimeToLive = 3600, + Resource = new DnsDomainResource { Entries = "example.com" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.CNAME, + Host = "wwwtest", + TimeToLive = 3600, + Resource = new DnsDomainResource { Entries = "www" } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.A, + Host = "mail", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.3") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.A, + Host = "mail2", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.4") } + }); + + originalZone.Records.Add(new DnsResourceRecord + { + Class = DnsQueryClass.IN, + Type = DnsQueryType.A, + Host = "mail3", + TimeToLive = 3600, + Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.5") } + }); + + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 4096, true)) + { + originalZone.SerializeZone(sw); + } + + var clonedZone = new DnsZone(); + + stream.Position = 0; + using (var sr = new StreamReader(stream, Encoding.UTF8, true, 4096)) + { + clonedZone.DeserializeZone(sr); + } + + Assert.Equal(originalZone.Records, clonedZone.Records); + } + } +}