diff --git a/TechnitiumLibrary.Net/Dns/ResourceRecords/DnsNAPTRRecordData.cs b/TechnitiumLibrary.Net/Dns/ResourceRecords/DnsNAPTRRecordData.cs index 5f6bc4b3..fce1803a 100644 --- a/TechnitiumLibrary.Net/Dns/ResourceRecords/DnsNAPTRRecordData.cs +++ b/TechnitiumLibrary.Net/Dns/ResourceRecords/DnsNAPTRRecordData.cs @@ -1,6 +1,7 @@ /* Technitium Library Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) +Copyright (C) 2026 Zafer Balkan (zafer@zaferbalkan.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +23,7 @@ You should have received a copy of the GNU General Public License using System.IO; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using TechnitiumLibrary.IO; @@ -46,6 +48,75 @@ public class DnsNAPTRRecordData : DnsResourceRecordData public DnsNAPTRRecordData(ushort order, ushort preference, string flags, string services, string regexp, string replacement) { + ArgumentNullException.ThrowIfNull(flags); + + ArgumentNullException.ThrowIfNull(services); + + ArgumentNullException.ThrowIfNull(regexp); + + ArgumentNullException.ThrowIfNull(replacement); + + // RFC 3403: REGEXP and REPLACEMENT are mutually exclusive + // If both are present (non-empty), the record is in error and MUST be rejected or ignored. + if (regexp.Length > 0 && replacement.Length > 0) + throw new ArgumentException( + "REGEXP and REPLACEMENT are mutually exclusive per RFC 3403."); + + // RFC 3403: FLAGS validation + // Flags are single characters from A–Z and 0–9, case-insensitive. + for (int i = 0; i < flags.Length; i++) + { + char c = flags[i]; + if (!(char.IsAsciiLetter(c) || char.IsDigit(c))) + throw new ArgumentException( + $"Invalid NAPTR flag '{c}'. Allowed set is A–Z and 0–9.", + nameof(flags)); + } + + // RFC 3403: SERVICES is a DNS + // RFC intentionally does NOT define semantics here; only basic sanity checks. + // Enforce non-control UTF-16 chars; deeper validation is application-specific. + for (int i = 0; i < services.Length; i++) + { + if (char.IsControl(services[i])) + throw new ArgumentException( + "SERVICES contains control characters, which are not permitted.", + nameof(services)); + } + + // RFC 3403: REPLACEMENT must be a fully qualified domain name + if (replacement.Length > 0) + { + // Must end with a root label (trailing dot) + if (!replacement.EndsWith(".", StringComparison.Ordinal)) + throw new ArgumentException( + "REPLACEMENT must be a fully qualified domain name ending with a dot.", + nameof(replacement)); + + // No name compression, no empty labels except the root + if (replacement.Contains("..", StringComparison.Ordinal)) + throw new ArgumentException( + "REPLACEMENT contains empty DNS labels.", + nameof(replacement)); + } + + // RFC 3403: REGEXP sanity + // The RFC requires POSIX ERE semantics but does not mandate compile-time validation. + // Enforce only that it is non-empty when used. + if (regexp.Length == 0 && replacement.Length == 0) + throw new ArgumentException( + "Either REGEXP or REPLACEMENT must be specified per RFC 3403."); + + // OPTIONAL: Validate regex, not defined in RFC. + // It is a NICE-TO-HAVE, not a MUST. + if (!IsValidRegex(regexp)) + { + throw new ArgumentException( + "REGEXP is not a valid regular expression.", + nameof(regexp)); + } + + // DNS constraints if (DnsClient.IsDomainNameUnicode(replacement)) replacement = DnsClient.ConvertDomainNameToAscii(replacement); @@ -84,6 +155,23 @@ private void Serialize() } } + private bool IsValidRegex(string pattern) + { + if (pattern is null) return false; + try + { + _ = new Regex( + pattern, + RegexOptions.None, + TimeSpan.FromMilliseconds(100)); + return true; + } + catch (ArgumentException) + { + return false; + } + } + #endregion #region protected