From 856a0d08033f3024ad46a965aa8bee59e6a481cd Mon Sep 17 00:00:00 2001 From: Alan Edwardes Date: Sat, 10 Feb 2024 22:15:07 +0000 Subject: [PATCH] Improves robustness of zone parsing. --- .../Records/DnsResourceRecord.cs | 40 ++++++++++++++--- src/Ae.Dns.Protocol/Records/DnsSoaResource.cs | 2 +- .../Records/DnsTextResource.cs | 1 - src/Ae.Dns.Protocol/Zone/DnsZone.cs | 45 ++++++++++++++++++- tests/Ae.Dns.Tests/Ae.Dns.Tests.csproj | 6 +++ tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs | 39 ++++++++++++++-- tests/Ae.Dns.Tests/Zone/test1.zone | 17 +++++++ tests/Ae.Dns.Tests/Zone/test2.zone | 11 +++++ 8 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 tests/Ae.Dns.Tests/Zone/test1.zone create mode 100644 tests/Ae.Dns.Tests/Zone/test2.zone diff --git a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs index a4cacd5..015da2d 100644 --- a/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs +++ b/src/Ae.Dns.Protocol/Records/DnsResourceRecord.cs @@ -129,14 +129,42 @@ public string ToZone(IDnsZone zone) /// public void FromZone(IDnsZone zone, string input) { - var parts = input.Split(null, 5); + var parts = input.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); - Host = zone.FromFormattedHost(parts[0]); - TimeToLive = uint.Parse(parts[1]); - Class = (DnsQueryClass)Enum.Parse(typeof(DnsQueryClass), parts[2]); - Type = (DnsQueryType)Enum.Parse(typeof(DnsQueryType), parts[3]); + var index = 0; + + if (char.IsWhiteSpace(input.First())) + { + Host = zone.Records.Last().Host; + } + else + { + Host = zone.FromFormattedHost(parts[index++]); + } + + if (uint.TryParse(parts[index], out var ttl)) + { + TimeToLive = ttl; + index++; + } + else + { + TimeToLive = (uint)zone.DefaultTtl.TotalSeconds; + } + + if (Enum.TryParse(parts[index], out var cl)) + { + Class = cl; + index++; + } + else + { + Class = zone.Records.Last().Class; + } + + Type = (DnsQueryType)Enum.Parse(typeof(DnsQueryType), parts[index++]); Resource = CreateResourceRecord(Type); - Resource.FromZone(zone, parts[4]); + Resource.FromZone(zone, string.Join(" ", parts.Skip(index))); } } } diff --git a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs index df67a21..7fb4b3f 100644 --- a/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsSoaResource.cs @@ -102,7 +102,7 @@ public void FromZone(IDnsZone zone, string input) MName = parts[0].Trim('.'); RName = parts[1].Trim('.'); - var parts1 = parts[2].Trim(new[] { '(', ')' }).Split(null); + var parts1 = parts[2].Trim(new[] { '(', ')' }).Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); Serial = uint.Parse(parts1[0]); Refresh = TimeSpan.FromSeconds(int.Parse(parts1[1])); Retry = TimeSpan.FromSeconds(int.Parse(parts1[2])); diff --git a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs index 7d64b5a..d9e7aa9 100644 --- a/src/Ae.Dns.Protocol/Records/DnsTextResource.cs +++ b/src/Ae.Dns.Protocol/Records/DnsTextResource.cs @@ -1,5 +1,4 @@ using Ae.Dns.Protocol.Zone; -using System; using System.Linq; namespace Ae.Dns.Protocol.Records diff --git a/src/Ae.Dns.Protocol/Zone/DnsZone.cs b/src/Ae.Dns.Protocol/Zone/DnsZone.cs index ca0a785..10166a4 100644 --- a/src/Ae.Dns.Protocol/Zone/DnsZone.cs +++ b/src/Ae.Dns.Protocol/Zone/DnsZone.cs @@ -1,5 +1,4 @@ -using Ae.Dns.Protocol.Enums; -using Ae.Dns.Protocol.Records; +using Ae.Dns.Protocol.Records; using System; using System.Collections.Generic; using System.IO; @@ -19,6 +18,22 @@ public sealed class DnsZone : IDnsZone private readonly List _records = new List(); private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + /// + /// Construct a new with no records. + /// + public DnsZone() + { + } + + /// + /// Construct a new with the specified records. + /// + /// + public DnsZone(IEnumerable records) + { + _records = records.ToList(); + } + /// public IReadOnlyList Records => _records; @@ -68,9 +83,35 @@ public void DeserializeZone(string zone) var reader = new StringReader(zone); + string? spillage = null; string? line; + bool spillover = false; while ((line = reader.ReadLine()) != null) { + line = line.Split(';')[0]; + + if (line.Contains("(") && !line.Contains(")")) + { + spillover = true; + } + + if (line.Contains(")")) + { + line = spillage + line; + spillover = false; + } + + if (spillover) + { + spillage += line; + continue; + } + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + if (line.StartsWith("$ORIGIN")) { Origin = line.Substring("$ORIGIN".Length).Trim().Trim('.'); diff --git a/tests/Ae.Dns.Tests/Ae.Dns.Tests.csproj b/tests/Ae.Dns.Tests/Ae.Dns.Tests.csproj index 6bb66a4..9b70431 100644 --- a/tests/Ae.Dns.Tests/Ae.Dns.Tests.csproj +++ b/tests/Ae.Dns.Tests/Ae.Dns.Tests.csproj @@ -50,4 +50,10 @@ + + + PreserveNewest + + + \ No newline at end of file diff --git a/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs index cd34830..95a58d9 100644 --- a/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs +++ b/tests/Ae.Dns.Tests/Zone/DnsZoneTests.cs @@ -2,6 +2,8 @@ using Ae.Dns.Protocol.Records; using Ae.Dns.Protocol.Zone; using System; +using System.IO; +using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; @@ -11,10 +13,9 @@ namespace Ae.Dns.Tests.Zone [Obsolete] public sealed class DnsZoneTests { - [Fact] - public async Task TestRoundTripZone() + private IDnsZone _wikipediaExampleZone = new DnsZone(new[] { - var zone = await RoundTripRecords(new DnsResourceRecord + new DnsResourceRecord { Class = DnsQueryClass.IN, Type = DnsQueryType.SOA, @@ -129,7 +130,17 @@ public async Task TestRoundTripZone() Host = "mail3.example.com", TimeToLive = 3600, Resource = new DnsIpAddressResource { IPAddress = IPAddress.Parse("192.0.2.5") } - }); + } + }) + { + Origin = "example.com", + DefaultTtl = TimeSpan.FromHours(1) + }; + + [Fact] + public async Task TestRoundTripZone() + { + var zone = await RoundTripRecords(_wikipediaExampleZone.Records.ToArray()); Assert.NotNull(zone); } @@ -163,6 +174,26 @@ public async Task TestTxtResource() Assert.NotNull(zone); } + [Fact] + public async Task TestLoadZone1() + { + var zone = new DnsZone(); + zone.DeserializeZone(await File.ReadAllTextAsync("Zone/test1.zone")); + + Assert.Equal(zone.Records, _wikipediaExampleZone.Records); + + var serialized = await RoundTripRecords(zone.Records.ToArray()); + + Assert.NotNull(serialized); + } + + [Fact] + public async Task TestLoadZone2() + { + var zone = new DnsZone(); + zone.DeserializeZone(await File.ReadAllTextAsync("Zone/test2.zone")); + } + public async Task RoundTripRecords(params DnsResourceRecord[] records) { var originalZone = new DnsZone diff --git a/tests/Ae.Dns.Tests/Zone/test1.zone b/tests/Ae.Dns.Tests/Zone/test1.zone new file mode 100644 index 0000000..c2db598 --- /dev/null +++ b/tests/Ae.Dns.Tests/Zone/test1.zone @@ -0,0 +1,17 @@ +$ORIGIN example.com. ; designates the start of this zone file in the namespace +$TTL 3600 ; default expiration time (in seconds) of all RRs without their own TTL value +example.com. IN SOA ns.example.com. username.example.com. ( 2020091025 7200 3600 1209600 3600 ) +example.com. IN NS ns ; ns.example.com is a nameserver for example.com +example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com +example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com +@ IN MX 20 mail2.example.com. ; equivalent to above line, "@" represents zone origin +@ IN MX 50 mail3 ; equivalent to above line, but using a relative host name +example.com. IN A 192.0.2.1 ; IPv4 address for example.com + IN AAAA 2001:db8:10::1 ; IPv6 address for example.com +ns IN A 192.0.2.2 ; IPv4 address for ns.example.com + IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com +www IN CNAME example.com. ; www.example.com is an alias for example.com +wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com +mail IN A 192.0.2.3 ; IPv4 address for mail.example.com +mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com +mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com \ No newline at end of file diff --git a/tests/Ae.Dns.Tests/Zone/test2.zone b/tests/Ae.Dns.Tests/Zone/test2.zone new file mode 100644 index 0000000..cc0309f --- /dev/null +++ b/tests/Ae.Dns.Tests/Zone/test2.zone @@ -0,0 +1,11 @@ +$ORIGIN localhost. +@ 86400 IN SOA @ root ( + 1999010100 ; serial + 10800 ; refresh (3 hours) + 900 ; retry (15 minutes) + 604800 ; expire (1 week) + 86400 ; minimum (1 day) + ) +@ 86400 IN NS @ +@ 86400 IN A 127.0.0.1 +@ 86400 IN AAAA ::1 \ No newline at end of file