Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions TechnitiumLibrary.Net/Dns/ResourceRecords/DnsNAPTRRecordData.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;

Expand All @@ -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 <character-string>
// 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 <character-string> constraints
if (DnsClient.IsDomainNameUnicode(replacement))
replacement = DnsClient.ConvertDomainNameToAscii(replacement);

Expand Down Expand Up @@ -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
Expand Down