mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
Option semantics: handlers, shared syntax, RaParam interface validation
- Add IOptionSemanticHandler implementations for DNS/DHCP/auth/interface options - Add shared syntax helpers (DnsmasqRrTypeSyntax, DnsmasqScopedDomainSyntax, etc.) - Wire semantics and registry for multi/single options; metadata-only semantics - RaParam: use DnsmasqRelaySyntax.IsInterfaceNameWithOptionalTrailingWildcard so interface names allow only trailing *, not asterisks in the middle (eth*0, *eth0) - Tests: RaParam trailing-wildcard and invalid-asterisk cases
This commit is contained in:
@@ -43,6 +43,30 @@ public class OptionSemanticValidatorTests
|
|||||||
new DhcpBootSemanticHandler(),
|
new DhcpBootSemanticHandler(),
|
||||||
new SlaacSemanticHandler(),
|
new SlaacSemanticHandler(),
|
||||||
new PxeServiceSemanticHandler(),
|
new PxeServiceSemanticHandler(),
|
||||||
|
new DhcpCircuitidSemanticHandler(),
|
||||||
|
new DhcpRemoteidSemanticHandler(),
|
||||||
|
new DhcpSubscridSemanticHandler(),
|
||||||
|
new FilterRrSemanticHandler(),
|
||||||
|
new CacheRrSemanticHandler(),
|
||||||
|
new InterfaceNameSemanticHandler(),
|
||||||
|
new AuthServerSemanticHandler(),
|
||||||
|
new CnameSemanticHandler(),
|
||||||
|
new MxHostSemanticHandler(),
|
||||||
|
new PtrRecordSemanticHandler(),
|
||||||
|
new InterfaceNameRecordSemanticHandler(),
|
||||||
|
new CaaRecordSemanticHandler(),
|
||||||
|
new SrvHostSemanticHandler(),
|
||||||
|
new NaptrRecordSemanticHandler(),
|
||||||
|
new DnsRrSemanticHandler(),
|
||||||
|
new DynamicHostSemanticHandler(),
|
||||||
|
new AuthSoaSemanticHandler(),
|
||||||
|
new AuthSecServersSemanticHandler(),
|
||||||
|
new AuthPeerSemanticHandler(),
|
||||||
|
new HostRecordSemanticHandler(),
|
||||||
|
new TxtRecordSemanticHandler(),
|
||||||
|
new DomainSemanticHandler(),
|
||||||
|
new SynthDomainSemanticHandler(),
|
||||||
|
new AuthZoneSemanticHandler(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -457,6 +481,9 @@ public class OptionSemanticValidatorTests
|
|||||||
[InlineData("eth0,60", true)]
|
[InlineData("eth0,60", true)]
|
||||||
[InlineData("eth0,mtu:1280,low,60,1200", true)]
|
[InlineData("eth0,mtu:1280,low,60,1200", true)]
|
||||||
[InlineData("eth0,high", true)]
|
[InlineData("eth0,high", true)]
|
||||||
|
[InlineData("eth*,60", true)]
|
||||||
|
[InlineData("eth*0,60", false)]
|
||||||
|
[InlineData("*eth0,60", false)]
|
||||||
[InlineData(",60", false)]
|
[InlineData(",60", false)]
|
||||||
[InlineData("eth0,mtu:", false)]
|
[InlineData("eth0,mtu:", false)]
|
||||||
public void ValidateMultiItem_RaParam_UsesHandler(string value, bool valid)
|
public void ValidateMultiItem_RaParam_UsesHandler(string value, bool valid)
|
||||||
@@ -655,4 +682,257 @@ public class OptionSemanticValidatorTests
|
|||||||
dir.Delete(recursive: true);
|
dir.Delete(recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_DhcpCircuitid_RequiresSetTagValue()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpCircuitid, "set:uplink,aa:bb:cc", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpCircuitid, "set:mytag,simple-string", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpCircuitid, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpCircuitid, "tag:uplink,id", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpCircuitid, "set:only", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_DhcpRemoteid_RequiresSetTagValue()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpRemoteid, "set:uplink,remote-id", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpRemoteid, "set:tag,", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_DhcpSubscrid_RequiresSetTagValue()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpSubscrid, "set:sub,subscriber-id", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DhcpSubscrid, "bad", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_FilterRr_AcceptsRrTypes()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "A", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "A,AAAA,TXT,MX", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "ANY", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "255", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.FilterRr, "A,,B", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_CacheRr_AcceptsRrTypes()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.CacheRr, "TXT", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.CacheRr, "A,AAAA,CNAME,SRV", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.CacheRr, "ANY", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.CacheRr, "", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_InterfaceNameOptions_AcceptInterfaceNameWithOptionalTrailingWildcard()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
foreach (var key in new[] { DnsmasqConfKeys.Interface, DnsmasqConfKeys.ExceptInterface, DnsmasqConfKeys.NoDhcpInterface, DnsmasqConfKeys.NoDhcpv4Interface, DnsmasqConfKeys.NoDhcpv6Interface })
|
||||||
|
{
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(key, "eth0", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(key, "eth0*", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(key, "br-lan", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(key, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(key, "eth*0", semantics));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_AuthServer_RequiresDomain_AcceptsInterfaceOrIp()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "example.com,eth0", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "example.com,192.168.1.1,eth1/4", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "ns.example.com,::1,eth0/6", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, ",eth0", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthServer, "example.com,invalid@", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_Cname_RequiresCnameAndTarget_AcceptsOptionalTtl()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "router.example.local,router", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "cname1,cname2,target", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "alias,host.example.com,60", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "only", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Cname, "bad..name,target", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_MxHost_AcceptsMxNameWithOptionalHostnameAndPreference()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "example.local", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "example.local,mail.example.local,10", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "mx.local,5", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "bad..name", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.MxHost, "mx.local,mail,99999", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_PtrRecord_NameAndOptionalTarget()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.PtrRecord, "1.0.168.192.in-addr.arpa,host.example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.PtrRecord, "name.only", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.PtrRecord, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.PtrRecord, "bad..name,target", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_InterfaceNameRecord_NameAndInterface()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.InterfaceName, "router.example.local,eth0", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.InterfaceName, "host.local,eth1/4", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.InterfaceName, "name", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.InterfaceName, "name,", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_CaaRecord_FourParts()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.CaaRecord, "example.com,0,issue,letsencrypt.org", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.CaaRecord, "a,b", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.CaaRecord, "x,y,z,w", semantics)); // 4 but invalid name possible - "x" is valid DNS name
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.CaaRecord, "x,256,tag,val", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_SrvHost_ServiceNameAndOptionalTargetPortPriorityWeight()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Srv, "_sip._udp.example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Srv, "_sip._udp,sip.example.com,5060,0,0", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Srv, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Srv, "_sip._udp,host,70000", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_NaptrRecord_SixOrSevenParts()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.NaptrRecord, "name.example.com,0,0,s,a,x,replacement", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.NaptrRecord, "n.zone,1,10,flags,svc,regex", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.NaptrRecord, "a,b,c", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.NaptrRecord, "bad..name,0,0,f,s,r", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_DnsRr_NameAndRrNumber_OptionalHex()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DnsRr, "example.com,255", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DnsRr, "name.example,1,01:02:03", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DnsRr, "x", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DnsRr, "x,99999", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_DynamicHost_FourFields_NameAndInterface()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DynamicHost, "example.com,0.0.0.8,eth0", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DynamicHost, "example.com,,,eth0", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.DynamicHost, "host.local,0.0.0.8,,eth0", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DynamicHost, "x,y", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.DynamicHost, "x,1.2.3.4,::1,bad if", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_AuthSoa_SerialAndOptionalFields()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSoa, "1", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSoa, "1,hostmaster.example.com,3600,600,86400", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSoa, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSoa, "x", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_AuthSecServers_OneOrMoreDomains()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSecServers, "ns1.example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSecServers, "a.com,b.com", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSecServers, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthSecServers, "bad..name", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_AuthPeer_OneOrMoreIps()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthPeer, "192.168.1.1", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthPeer, "::1,10.0.0.1", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthPeer, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthPeer, "not-an-ip", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_HostRecord_AtLeastOneName_OptionalIpv4Ipv6Ttl()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.HostRecord, "laptop,192.168.0.1", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.HostRecord, "host.example.com,192.168.0.1,::1,60", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.HostRecord, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.HostRecord, "bad..name,1.2.3.4", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_TxtRecord_NameRequired()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.TxtRecord, "example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.TxtRecord, "name.example.com,some text", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.TxtRecord, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.TxtRecord, "bad..name,text", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_Domain_DomainOrHash_OptionalRangeOrInterface()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "thekelleys.org.uk", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "#", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "lan,192.168.0.0/24,local", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "local.zone,eth0", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.Domain, "bad..name", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_SynthDomain_DomainAndRange_OptionalPrefix()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.SynthDomain, "zone.example.com,192.168.0.50,192.168.0.70", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.SynthDomain, "zone,192.168.0.0/24,internal-*", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.SynthDomain, "only", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.SynthDomain, "bad..name,1.2.3.4", semantics));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateMultiItem_AuthZone_DomainRequired_OptionalSubnets()
|
||||||
|
{
|
||||||
|
var semantics = new OptionValidationSemantics(OptionValidationKind.Complex);
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "example.com", semantics));
|
||||||
|
Assert.Null(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "zone,192.168.0.0/24", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "", semantics));
|
||||||
|
Assert.NotNull(_validator.ValidateMultiItem(DnsmasqConfKeys.AuthZone, "bad..name", semantics));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ public static class DnsmasqOptionPlaceholders
|
|||||||
[DnsmasqConfKeys.RaParam] = "eth0,mtu:1500,high",
|
[DnsmasqConfKeys.RaParam] = "eth0,mtu:1500,high",
|
||||||
[DnsmasqConfKeys.Slaac] = "eth0,::10",
|
[DnsmasqConfKeys.Slaac] = "eth0,::10",
|
||||||
[DnsmasqConfKeys.DhcpRelay] = "192.168.1.0,192.168.2.1",
|
[DnsmasqConfKeys.DhcpRelay] = "192.168.1.0,192.168.2.1",
|
||||||
[DnsmasqConfKeys.DhcpCircuitid] = "tag:uplink,example-circuit",
|
[DnsmasqConfKeys.DhcpCircuitid] = "set:<tag>,<circuit-id>",
|
||||||
[DnsmasqConfKeys.DhcpRemoteid] = "tag:uplink,example-remote",
|
[DnsmasqConfKeys.DhcpRemoteid] = "set:<tag>,<remote-id>",
|
||||||
[DnsmasqConfKeys.DhcpSubscrid] = "tag:uplink,example-subscriber",
|
[DnsmasqConfKeys.DhcpSubscrid] = "set:<tag>,<subscriber-id>",
|
||||||
[DnsmasqConfKeys.DhcpProxy] = "192.168.1.0,192.168.1.1",
|
[DnsmasqConfKeys.DhcpProxy] = "192.168.1.0,192.168.1.1",
|
||||||
[DnsmasqConfKeys.TagIf] = "tag:guest,tag:wifi",
|
[DnsmasqConfKeys.TagIf] = "tag:guest,tag:wifi",
|
||||||
[DnsmasqConfKeys.BridgeInterface] = "br0,eth0",
|
[DnsmasqConfKeys.BridgeInterface] = "br0,eth0",
|
||||||
|
|||||||
+140
@@ -346,6 +346,146 @@ public static class EffectiveConfigSpecialOptionSemantics
|
|||||||
EffectiveConfigParserBehavior.Multi,
|
EffectiveConfigParserBehavior.Multi,
|
||||||
EffectiveConfigWriteBehavior.MultiValue,
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
ComplexMulti),
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.DhcpCircuitid] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.DhcpCircuitid,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.DhcpRemoteid] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.DhcpRemoteid,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.DhcpSubscrid] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.DhcpSubscrid,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.FilterRr] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.FilterRr,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.CacheRr] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.CacheRr,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.Interface] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Interface,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.ExceptInterface] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.ExceptInterface,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.NoDhcpInterface] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.NoDhcpInterface,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.NoDhcpv4Interface] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.NoDhcpv4Interface,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.NoDhcpv6Interface] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.NoDhcpv6Interface,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.AuthServer] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AuthServer,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.Cname] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Cname,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.MxHost] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.MxHost,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.PtrRecord] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.PtrRecord,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.InterfaceName] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.InterfaceName,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.CaaRecord] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.CaaRecord,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.Srv] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Srv,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.NaptrRecord] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.NaptrRecord,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.Domain] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Domain,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.TxtRecord] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.TxtRecord,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.HostRecord] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.HostRecord,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.DynamicHost] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.DynamicHost,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.DnsRr] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.DnsRr,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.SynthDomain] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.SynthDomain,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.AuthZone] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AuthZone,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.AuthSoa] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AuthSoa,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.AuthSecServers] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AuthSecServers,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
|
[DnsmasqConfKeys.AuthPeer] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AuthPeer,
|
||||||
|
EffectiveConfigParserBehavior.Multi,
|
||||||
|
EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
ComplexMulti),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>Keys (enabled, disabled) for InversePair options only. Used by write path and readonly hints.</summary>
|
/// <summary>Keys (enabled, disabled) for InversePair options only. Used by write path and readonly hints.</summary>
|
||||||
|
|||||||
+10
-10
@@ -230,29 +230,29 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
|
|||||||
DnsmasqConfKeys.DhcpUserclass,
|
DnsmasqConfKeys.DhcpUserclass,
|
||||||
DnsmasqConfKeys.Slaac);
|
DnsmasqConfKeys.Slaac);
|
||||||
|
|
||||||
RegisterMultis(
|
RegisterSemanticMultis(
|
||||||
EffectiveConfigFieldBuilder.SectionResolver,
|
EffectiveConfigFieldBuilder.SectionResolver,
|
||||||
DnsmasqConfKeys.FilterRr);
|
DnsmasqConfKeys.FilterRr);
|
||||||
RegisterMultis(
|
RegisterSemanticMultis(
|
||||||
EffectiveConfigFieldBuilder.SectionDnsRecords,
|
EffectiveConfigFieldBuilder.SectionDnsRecords,
|
||||||
DnsmasqConfKeys.Domain,
|
DnsmasqConfKeys.Domain,
|
||||||
DnsmasqConfKeys.Cname,
|
DnsmasqConfKeys.Cname,
|
||||||
DnsmasqConfKeys.MxHost,
|
DnsmasqConfKeys.MxHost,
|
||||||
DnsmasqConfKeys.Srv,
|
|
||||||
DnsmasqConfKeys.PtrRecord,
|
DnsmasqConfKeys.PtrRecord,
|
||||||
DnsmasqConfKeys.TxtRecord,
|
|
||||||
DnsmasqConfKeys.NaptrRecord,
|
|
||||||
DnsmasqConfKeys.HostRecord,
|
|
||||||
DnsmasqConfKeys.DynamicHost,
|
|
||||||
DnsmasqConfKeys.InterfaceName,
|
DnsmasqConfKeys.InterfaceName,
|
||||||
DnsmasqConfKeys.CaaRecord,
|
DnsmasqConfKeys.CaaRecord,
|
||||||
|
DnsmasqConfKeys.Srv,
|
||||||
|
DnsmasqConfKeys.NaptrRecord,
|
||||||
|
DnsmasqConfKeys.TxtRecord,
|
||||||
|
DnsmasqConfKeys.HostRecord,
|
||||||
|
DnsmasqConfKeys.DynamicHost,
|
||||||
DnsmasqConfKeys.DnsRr,
|
DnsmasqConfKeys.DnsRr,
|
||||||
DnsmasqConfKeys.SynthDomain,
|
DnsmasqConfKeys.SynthDomain,
|
||||||
DnsmasqConfKeys.AuthZone,
|
DnsmasqConfKeys.AuthZone,
|
||||||
DnsmasqConfKeys.AuthSoa,
|
DnsmasqConfKeys.AuthSoa,
|
||||||
DnsmasqConfKeys.AuthSecServers,
|
DnsmasqConfKeys.AuthSecServers,
|
||||||
DnsmasqConfKeys.AuthPeer);
|
DnsmasqConfKeys.AuthPeer);
|
||||||
RegisterMultis(
|
RegisterSemanticMultis(
|
||||||
EffectiveConfigFieldBuilder.SectionDhcp,
|
EffectiveConfigFieldBuilder.SectionDhcp,
|
||||||
DnsmasqConfKeys.DhcpCircuitid,
|
DnsmasqConfKeys.DhcpCircuitid,
|
||||||
DnsmasqConfKeys.DhcpRemoteid,
|
DnsmasqConfKeys.DhcpRemoteid,
|
||||||
@@ -264,8 +264,8 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
|
|||||||
EffectiveConfigFieldBuilder.SectionTftpPxe,
|
EffectiveConfigFieldBuilder.SectionTftpPxe,
|
||||||
DnsmasqConfKeys.DhcpOptionPxe);
|
DnsmasqConfKeys.DhcpOptionPxe);
|
||||||
RegisterSemanticMultis(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.TrustAnchor);
|
RegisterSemanticMultis(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.TrustAnchor);
|
||||||
RegisterMultis(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheRr);
|
RegisterSemanticMultis(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheRr);
|
||||||
RegisterMultis(
|
RegisterSemanticMultis(
|
||||||
EffectiveConfigFieldBuilder.SectionProcess,
|
EffectiveConfigFieldBuilder.SectionProcess,
|
||||||
DnsmasqConfKeys.Interface,
|
DnsmasqConfKeys.Interface,
|
||||||
DnsmasqConfKeys.ExceptInterface,
|
DnsmasqConfKeys.ExceptInterface,
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Net;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>auth-peer</c> values: <ip-address>[,<ip-address>...].
|
||||||
|
/// One or more IP addresses (secondary servers allowed to initiate AXFR).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthPeerSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.AuthPeer;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "auth-peer value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens.Any(t => t.Length == 0))
|
||||||
|
return "auth-peer must contain at least one IP address.";
|
||||||
|
foreach (var t in tokens)
|
||||||
|
{
|
||||||
|
if (!IPAddress.TryParse(t, out _))
|
||||||
|
return "auth-peer each value must be a valid IP address.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>auth-sec-servers</c> values: <domain>[,<domain>...].
|
||||||
|
/// One or more domain names (secondary servers for authoritative zones).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthSecServersSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.AuthSecServers;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "auth-sec-servers value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens.Any(t => t.Length == 0))
|
||||||
|
return "auth-sec-servers must contain at least one domain name.";
|
||||||
|
foreach (var t in tokens)
|
||||||
|
{
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(t))
|
||||||
|
return "auth-sec-servers each domain must be a valid DNS name.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Net;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>auth-server</c> values: <domain>,[<interface>|<ip-address>...].
|
||||||
|
/// Domain (glue record) is required; optional comma-separated list of interfaces or IPs; interface may have /4 or /6.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthServerSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.AuthServer;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "auth-server value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens[0].Length == 0)
|
||||||
|
return "auth-server requires a domain (glue record).";
|
||||||
|
for (var i = 1; i < tokens.Length; i++)
|
||||||
|
{
|
||||||
|
var token = tokens[i];
|
||||||
|
if (token.Length == 0)
|
||||||
|
return "auth-server list cannot contain empty interface or address.";
|
||||||
|
if (IsIpOrInterface(token))
|
||||||
|
continue;
|
||||||
|
return "auth-server: each item after the domain must be an IP address or interface name (optional /4 or /6).";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIpOrInterface(string token)
|
||||||
|
{
|
||||||
|
if (IPAddress.TryParse(token, out _))
|
||||||
|
return true;
|
||||||
|
if (token.EndsWith("/4", StringComparison.Ordinal) || token.EndsWith("/6", StringComparison.Ordinal))
|
||||||
|
return DnsmasqRelaySyntax.IsInterfaceName(token[..^2]);
|
||||||
|
return DnsmasqRelaySyntax.IsInterfaceName(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>auth-soa</c> values: <serial>[,<hostmaster>[,<refresh>[,<retry>[,<expiry>]]]].
|
||||||
|
/// Serial required; optional hostmaster (DNS name), refresh, retry, expiry (numbers).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthSoaSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.AuthSoa;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "auth-soa value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is 0 or > 5)
|
||||||
|
return "auth-soa must be <serial>[,<hostmaster>[,<refresh>[,<retry>[,<expiry>]]]].";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "auth-soa fields cannot be empty.";
|
||||||
|
if (!uint.TryParse(tokens[0], NumberStyles.None, CultureInfo.InvariantCulture, out _))
|
||||||
|
return "auth-soa serial must be a number.";
|
||||||
|
if (tokens.Length >= 2 && !DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[1]))
|
||||||
|
return "auth-soa hostmaster must be a valid DNS name.";
|
||||||
|
for (var i = 2; i < tokens.Length; i++)
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(tokens[i], NumberStyles.None, CultureInfo.InvariantCulture, out _))
|
||||||
|
return "auth-soa refresh, retry and expiry must be numbers.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>auth-zone</c> values: <domain>[,<subnet>[/prefix]...][,exclude:<subnet>...].
|
||||||
|
/// Domain required; optional subnets or exclude: clauses (interface names or CIDR).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AuthZoneSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.AuthZone;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "auth-zone value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens[0].Length == 0)
|
||||||
|
return "auth-zone requires a domain name.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "auth-zone domain must be a valid DNS name.";
|
||||||
|
for (var i = 1; i < tokens.Length; i++)
|
||||||
|
{
|
||||||
|
if (tokens[i].Length == 0)
|
||||||
|
return "auth-zone cannot have empty subnet or exclude fields.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>caa-record</c> values: <name>,<flags>,<tag>,<value> (RFC6844).
|
||||||
|
/// All four parts required.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CaaRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.CaaRecord;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "caa-record value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length != 4)
|
||||||
|
return "caa-record must be <name>,<flags>,<tag>,<value>.";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "caa-record name, flags, tag and value cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "caa-record name must be a valid DNS name.";
|
||||||
|
if (!byte.TryParse(tokens[1], out _))
|
||||||
|
return "caa-record flags must be 0-255.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>cache-rr</c> values: RR-type or comma-separated list (e.g. A,AAAA,TXT or ANY).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CacheRrSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.CacheRr;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "cache-rr value cannot be empty.";
|
||||||
|
foreach (var token in s.Split(',', StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token.Length == 0)
|
||||||
|
return "cache-rr list cannot contain empty RR-type.";
|
||||||
|
if (!DnsmasqRrTypeSyntax.IsValidRrType(token))
|
||||||
|
return "cache-rr RR-type must be a name (e.g. A, TXT, ANY) or decimal number.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>cname</c> values: <cname>[,<cname>,]<target>[,<TTL>].
|
||||||
|
/// At least one cname and target required; optional TTL as positive integer.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CnameSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.Cname;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "cname value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length < 2)
|
||||||
|
return "cname must be <cname>,<target> or <cname>[,<cname>...],<target>[,<TTL>].";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "cname list cannot contain empty parts.";
|
||||||
|
var lastIsTtl = tokens.Length >= 3 && int.TryParse(tokens[^1], out var ttl) && ttl > 0;
|
||||||
|
var targetIndex = lastIsTtl ? tokens.Length - 2 : tokens.Length - 1;
|
||||||
|
if (targetIndex < 1)
|
||||||
|
return "cname requires at least one cname and a target.";
|
||||||
|
for (var i = 0; i <= targetIndex; i++)
|
||||||
|
{
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[i]))
|
||||||
|
return "cname and target must be valid DNS names (e.g. host.example.com).";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>dhcp-circuitid</c> values: set:<tag>,<circuit-id> (colon-hex or string).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DhcpCircuitidSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.DhcpCircuitid;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value) =>
|
||||||
|
DnsmasqDhcpTagSyntax.ValidateSetTagValue((value ?? "").Trim(), "dhcp-circuitid");
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>dhcp-remoteid</c> values: set:<tag>,<remote-id> (colon-hex or string).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DhcpRemoteidSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.DhcpRemoteid;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value) =>
|
||||||
|
DnsmasqDhcpTagSyntax.ValidateSetTagValue((value ?? "").Trim(), "dhcp-remoteid");
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>dhcp-subscrid</c> values: set:<tag>,<subscriber-id> (RFC3993).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DhcpSubscridSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.DhcpSubscrid;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value) =>
|
||||||
|
DnsmasqDhcpTagSyntax.ValidateSetTagValue((value ?? "").Trim(), "dhcp-subscrid");
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>dns-rr</c> values: <name>,<RR-number>,[<hex data>].
|
||||||
|
/// Name and RR type number required; optional hex data (digits, colons, spaces).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DnsRrSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.DnsRr;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "dns-rr value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', 3, StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length < 2)
|
||||||
|
return "dns-rr must be <name>,<RR-number>[,<hex data>].";
|
||||||
|
if (tokens[0].Length == 0)
|
||||||
|
return "dns-rr name cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "dns-rr name must be a valid DNS name.";
|
||||||
|
if (!ushort.TryParse(tokens[1], NumberStyles.None, CultureInfo.InvariantCulture, out _))
|
||||||
|
return "dns-rr RR-number must be 0-65535.";
|
||||||
|
if (tokens.Length == 3 && tokens[2].Length > 0 && !IsValidHexData(tokens[2]))
|
||||||
|
return "dns-rr hex data must be hex digits, colons or spaces (e.g. 01:23:45 or 012345).";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidHexData(string value) =>
|
||||||
|
value.All(c => char.IsAsciiHexDigit(c) || c is ':' or ' ' or '\t');
|
||||||
|
}
|
||||||
+21
@@ -36,4 +36,25 @@ internal static class DnsmasqDhcpTagSyntax
|
|||||||
token.Length > 0 &&
|
token.Length > 0 &&
|
||||||
token.Length <= 64 &&
|
token.Length <= 64 &&
|
||||||
token.All(c => char.IsLetterOrDigit(c) || c is '-' or '_' or '.' || (allowWildcard && c == '*'));
|
token.All(c => char.IsLetterOrDigit(c) || c is '-' or '_' or '.' || (allowWildcard && c == '*'));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates <c>set:<tag>,<value></c> form used by dhcp-circuitid, dhcp-remoteid, dhcp-subscrid.
|
||||||
|
/// Tag and value must be non-empty; value may be colon-separated hex or a simple string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Trimmed option value.</param>
|
||||||
|
/// <param name="optionLabel">Option name for error messages (e.g. "dhcp-circuitid").</param>
|
||||||
|
/// <returns>Error message or null if valid.</returns>
|
||||||
|
public static string? ValidateSetTagValue(string value, string optionLabel)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return $"{optionLabel} value cannot be empty.";
|
||||||
|
var tokens = value.Split(',', 2, StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length != 2)
|
||||||
|
return $"{optionLabel} must be set:<tag>,<value>.";
|
||||||
|
if (tokens[0].Length == 0 || tokens[1].Length == 0)
|
||||||
|
return $"{optionLabel} tag and value cannot be empty.";
|
||||||
|
if (!IsSetToken(tokens[0]))
|
||||||
|
return $"{optionLabel} must start with set:<tag>.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -31,4 +31,20 @@ internal static class DnsmasqRelaySyntax
|
|||||||
value.Length <= 64 &&
|
value.Length <= 64 &&
|
||||||
value.Count(c => c == '.') <= 1 &&
|
value.Count(c => c == '.') <= 1 &&
|
||||||
value.All(c => char.IsLetterOrDigit(c) || c is '-' or '_' or '.');
|
value.All(c => char.IsLetterOrDigit(c) || c is '-' or '_' or '.');
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface name with optional trailing <c>*</c> wildcard (for --interface, --except-interface, etc.).
|
||||||
|
/// Only a single trailing asterisk is allowed, not <c>*</c> in the middle.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsInterfaceNameWithOptionalTrailingWildcard(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return false;
|
||||||
|
var v = value.Trim();
|
||||||
|
if (v.Length == 0)
|
||||||
|
return false;
|
||||||
|
if (v.EndsWith('*'))
|
||||||
|
return v.Length > 1 && IsInterfaceName(v[..^1]);
|
||||||
|
return IsInterfaceName(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared helpers for DNS RR-type values used by <c>filter-rr</c> and <c>cache-rr</c>.
|
||||||
|
/// RR-type can be a name (e.g. TXT, MX, A, AAAA, ANY) or a decimal number.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DnsmasqRrTypeSyntax
|
||||||
|
{
|
||||||
|
/// <summary>Returns true if the token is a valid RR-type: non-empty, letters/digits/hyphen or decimal number.</summary>
|
||||||
|
public static bool IsValidRrType(string token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return false;
|
||||||
|
var t = token.Trim();
|
||||||
|
if (t.Length == 0)
|
||||||
|
return false;
|
||||||
|
return t.All(c => char.IsLetterOrDigit(c) || c == '-');
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -11,6 +11,27 @@ internal static partial class DnsmasqScopedDomainSyntax
|
|||||||
[GeneratedRegex(@"^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$", RegexOptions.CultureInvariant)]
|
[GeneratedRegex(@"^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$", RegexOptions.CultureInvariant)]
|
||||||
private static partial Regex DomainPattern();
|
private static partial Regex DomainPattern();
|
||||||
|
|
||||||
|
/// <summary>Returns true if the value is a valid DNS name (hostname/domain) for record options like cname, mx-host.</summary>
|
||||||
|
public static bool IsValidDnsName(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return false;
|
||||||
|
var v = value.Trim();
|
||||||
|
return v.Length >= 1 && v.Length <= 253 && DomainPattern().IsMatch(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns true if the value looks like an SRV service name (e.g. _sip._udp or _sip._udp.example.com). Allows leading underscore in labels.</summary>
|
||||||
|
public static bool IsValidSrvServiceName(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return false;
|
||||||
|
var v = value.Trim();
|
||||||
|
if (v.Length > 253)
|
||||||
|
return false;
|
||||||
|
return v.Split('.').All(label => label.Length > 0 && label.Length <= 63 &&
|
||||||
|
label.All(c => char.IsLetterOrDigit(c) || c is '-' or '_'));
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TrySplitScopedValue(string value, out string[] domains, out string tail, out string? error)
|
public static bool TrySplitScopedValue(string value, out string[] domains, out string tail, out string? error)
|
||||||
{
|
{
|
||||||
domains = Array.Empty<string>();
|
domains = Array.Empty<string>();
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Net;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>domain</c> (DHCP/DNS) values: <domain>[[,<address range>[,local]]|<interface>].
|
||||||
|
/// Domain required (# or DNS name); optional address range (IP or CIDR), interface, or "local".
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DomainSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.Domain;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "domain value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens[0].Length == 0)
|
||||||
|
return "domain requires a domain name or #.";
|
||||||
|
var domain = tokens[0];
|
||||||
|
if (domain != "#" && !DnsmasqScopedDomainSyntax.IsValidDnsName(domain))
|
||||||
|
return "domain must be a valid DNS name or #.";
|
||||||
|
if (tokens.Length >= 2 && tokens[1].Length == 0)
|
||||||
|
return "domain cannot have empty fields.";
|
||||||
|
if (tokens.Length >= 2 && !IsValidSecondToken(tokens[1]))
|
||||||
|
return "domain second field must be interface name, IP, CIDR (e.g. 192.168.0.0/24), or 'local'.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidSecondToken(string token)
|
||||||
|
{
|
||||||
|
if (token == "local")
|
||||||
|
return true;
|
||||||
|
if (DnsmasqRelaySyntax.IsInterfaceName(token))
|
||||||
|
return true;
|
||||||
|
if (token.Contains('/'))
|
||||||
|
return DnsmasqIpPrefixSyntax.ValidateIpWithOptionalPrefix(token, "domain") is null;
|
||||||
|
return IPAddress.TryParse(token, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Net;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>dynamic-host</c> values: <name>,[IPv4],[IPv6],<interface> (dnsmasq man).
|
||||||
|
/// Name and interface required. Doc example uses 3 tokens (name,IPv4,interface); we allow 3 or 4.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DynamicHostSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.DynamicHost;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "dynamic-host value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is not 3 and not 4)
|
||||||
|
return "dynamic-host must be <name>,[IPv4],[IPv6],<interface> (3 or 4 comma-separated fields).";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "dynamic-host name must be a valid DNS name.";
|
||||||
|
var interfaceIndex = tokens.Length - 1;
|
||||||
|
if (tokens[1].Length > 0 && !IPAddress.TryParse(tokens[1], out _))
|
||||||
|
return "dynamic-host IPv4 must be a valid address or empty.";
|
||||||
|
if (tokens.Length == 4 && tokens[2].Length > 0 && !IPAddress.TryParse(tokens[2], out _))
|
||||||
|
return "dynamic-host IPv6 must be a valid address or empty.";
|
||||||
|
if (tokens[interfaceIndex].Length == 0)
|
||||||
|
return "dynamic-host interface cannot be empty.";
|
||||||
|
if (!DnsmasqRelaySyntax.IsInterfaceName(tokens[interfaceIndex]))
|
||||||
|
return "dynamic-host interface must be a valid interface name.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>filter-rr</c> values: RR-type or comma-separated list (e.g. A,AAAA,TXT or ANY).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FilterRrSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.FilterRr;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "filter-rr value cannot be empty.";
|
||||||
|
foreach (var token in s.Split(',', StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
if (token.Length == 0)
|
||||||
|
return "filter-rr list cannot contain empty RR-type.";
|
||||||
|
if (!DnsmasqRrTypeSyntax.IsValidRrType(token))
|
||||||
|
return "filter-rr RR-type must be a name (e.g. A, TXT, ANY) or decimal number.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Net;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>host-record</c> values: <name>[,<name>...],[<IPv4>],[<IPv6>][,<TTL>].
|
||||||
|
/// At least one name required; optional IPv4, IPv6, TTL (positive integer).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HostRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.HostRecord;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "host-record value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length == 0 || tokens[0].Length == 0)
|
||||||
|
return "host-record requires at least one name.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "host-record name(s) must be valid DNS names.";
|
||||||
|
var lastIsTtl = tokens.Length >= 2 && int.TryParse(tokens[^1], out var ttl) && ttl > 0;
|
||||||
|
var lastIndex = lastIsTtl ? tokens.Length - 2 : tokens.Length - 1;
|
||||||
|
for (var i = 1; i <= lastIndex; i++)
|
||||||
|
{
|
||||||
|
var t = tokens[i];
|
||||||
|
if (t.Length == 0)
|
||||||
|
return "host-record cannot have empty fields.";
|
||||||
|
if (IPAddress.TryParse(t, out _))
|
||||||
|
continue;
|
||||||
|
if (int.TryParse(t, out _))
|
||||||
|
return "host-record TTL must be last and positive; names and addresses cannot be numeric-only.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(t))
|
||||||
|
return "host-record name must be a valid DNS name.";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>interface-name</c> (DNS record) values: <name>,<interface>[/4|/6].
|
||||||
|
/// Name and interface required; interface may have /4 or /6 suffix.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InterfaceNameRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.InterfaceName;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "interface-name value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length != 2)
|
||||||
|
return "interface-name must be <name>,<interface>[/4|/6].";
|
||||||
|
if (tokens[0].Length == 0 || tokens[1].Length == 0)
|
||||||
|
return "interface-name name and interface cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "interface-name name must be a valid DNS name.";
|
||||||
|
var iface = tokens[1];
|
||||||
|
if (iface.EndsWith("/4", StringComparison.Ordinal) || iface.EndsWith("/6", StringComparison.Ordinal))
|
||||||
|
iface = iface[..^2];
|
||||||
|
if (!DnsmasqRelaySyntax.IsInterfaceName(iface))
|
||||||
|
return "interface-name interface must be a valid interface name (optional /4 or /6).";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for interface-name options: <c>interface</c>, <c>except-interface</c>,
|
||||||
|
/// <c>no-dhcp-interface</c>, <c>no-dhcpv4-interface</c>, <c>no-dhcpv6-interface</c>.
|
||||||
|
/// Each value is an interface name with optional trailing <c>*</c> wildcard.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InterfaceNameSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> HandledOptions = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
DnsmasqConfKeys.Interface,
|
||||||
|
DnsmasqConfKeys.ExceptInterface,
|
||||||
|
DnsmasqConfKeys.NoDhcpInterface,
|
||||||
|
DnsmasqConfKeys.NoDhcpv4Interface,
|
||||||
|
DnsmasqConfKeys.NoDhcpv6Interface,
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool CanHandle(string optionName) => HandledOptions.Contains(optionName);
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "Interface name cannot be empty.";
|
||||||
|
return DnsmasqRelaySyntax.IsInterfaceNameWithOptionalTrailingWildcard(s)
|
||||||
|
? null
|
||||||
|
: "Must be an interface name (e.g. eth0) with optional trailing * wildcard.";
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>mx-host</c> values: <mx name>[[,<hostname>],<preference>].
|
||||||
|
/// MX name required; optional hostname and/or preference (integer).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MxHostSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.MxHost;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "mx-host value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is 0 or > 3)
|
||||||
|
return "mx-host must be <mx name> or <mx name>,<hostname> or <mx name>[,<hostname>],<preference>.";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "mx-host list cannot contain empty parts.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "mx-host MX name must be a valid DNS name.";
|
||||||
|
if (tokens.Length >= 2)
|
||||||
|
{
|
||||||
|
var lastIsPreference = int.TryParse(tokens[^1], out var pref) && pref >= 0 && pref <= 65535;
|
||||||
|
if (tokens.Length == 2)
|
||||||
|
{
|
||||||
|
if (!lastIsPreference && !DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[1]))
|
||||||
|
return "mx-host hostname or preference must be a valid DNS name or 0-65535.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!lastIsPreference)
|
||||||
|
return "mx-host preference must be 0-65535.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[1]))
|
||||||
|
return "mx-host hostname must be a valid DNS name.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>naptr-record</c> values: <name>,<order>,<preference>,<flags>,<service>,<regexp>[,<replacement>] (RFC3403).
|
||||||
|
/// Six required fields; optional replacement.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NaptrRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.NaptrRecord;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "naptr-record value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is < 6 or > 7)
|
||||||
|
return "naptr-record must be <name>,<order>,<preference>,<flags>,<service>,<regexp>[,<replacement>].";
|
||||||
|
if (tokens.Take(6).Any(t => t.Length == 0))
|
||||||
|
return "naptr-record name, order, preference, flags, service and regexp cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "naptr-record name must be a valid DNS name.";
|
||||||
|
if (!ushort.TryParse(tokens[1], out _))
|
||||||
|
return "naptr-record order must be 0-65535.";
|
||||||
|
if (!ushort.TryParse(tokens[2], out _))
|
||||||
|
return "naptr-record preference must be 0-65535.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>ptr-record</c> values: <name>[,<target>].
|
||||||
|
/// Name required; optional target (both must be valid DNS names).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PtrRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.PtrRecord;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "ptr-record value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is 0 or > 2)
|
||||||
|
return "ptr-record must be <name> or <name>,<target>.";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "ptr-record name and target cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "ptr-record name must be a valid DNS name.";
|
||||||
|
if (tokens.Length == 2 && !DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[1]))
|
||||||
|
return "ptr-record target must be a valid DNS name.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-5
@@ -23,8 +23,8 @@ public sealed class RaParamSemanticHandler : IOptionSemanticHandler
|
|||||||
if (tokens.Length == 0 || tokens.Any(t => t.Length == 0))
|
if (tokens.Length == 0 || tokens.Any(t => t.Length == 0))
|
||||||
return "ra-param contains an empty comma-separated segment.";
|
return "ra-param contains an empty comma-separated segment.";
|
||||||
|
|
||||||
if (!IsInterfaceName(tokens[0]))
|
if (!DnsmasqRelaySyntax.IsInterfaceNameWithOptionalTrailingWildcard(tokens[0]))
|
||||||
return "ra-param must start with an interface name.";
|
return "ra-param must start with an interface name (optionally ending with *).";
|
||||||
|
|
||||||
var seenPriority = false;
|
var seenPriority = false;
|
||||||
var seenMtu = false;
|
var seenMtu = false;
|
||||||
@@ -68,7 +68,4 @@ public sealed class RaParamSemanticHandler : IOptionSemanticHandler
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsInterfaceName(string value) =>
|
|
||||||
value.Length > 0 && value.All(c => char.IsLetterOrDigit(c) || c is '-' or '_' or '.' or '*');
|
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>srv-host</c> values: <_service>.<_prot>[.<domain>],[<target>[,<port>[,<priority>[,<weight>]]]].
|
||||||
|
/// Service name required; optional target, port (0-65535), priority, weight.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SrvHostSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.Srv;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "srv-host value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is 0 or > 5)
|
||||||
|
return "srv-host must be <_service>._prot[.domain][,target[,port[,priority[,weight]]]].";
|
||||||
|
if (tokens[0].Length == 0)
|
||||||
|
return "srv-host service name cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidSrvServiceName(tokens[0]))
|
||||||
|
return "srv-host service name must be like _service._prot or _service._prot.domain.";
|
||||||
|
if (tokens.Length >= 2 && tokens[1].Length > 0 && !DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[1]))
|
||||||
|
return "srv-host target must be a valid DNS name or empty.";
|
||||||
|
if (tokens.Length >= 3 && (!int.TryParse(tokens[2], out var port) || port < 0 || port > 65535))
|
||||||
|
return "srv-host port must be 0-65535.";
|
||||||
|
if (tokens.Length >= 4 && (!int.TryParse(tokens[3], out var pri) || pri < 0 || pri > 65535))
|
||||||
|
return "srv-host priority must be 0-65535.";
|
||||||
|
if (tokens.Length >= 5 && (!int.TryParse(tokens[4], out var w) || w < 0 || w > 65535))
|
||||||
|
return "srv-host weight must be 0-65535.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>synth-domain</c> values: <domain>,<address range>[,<prefix>[*]].
|
||||||
|
/// Domain and address range required; optional prefix (may end with *).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SynthDomainSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.SynthDomain;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "synth-domain value cannot be empty.";
|
||||||
|
var tokens = s.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
if (tokens.Length is < 2 or > 3)
|
||||||
|
return "synth-domain must be <domain>,<address range>[,<prefix>[*]].";
|
||||||
|
if (tokens.Any(t => t.Length == 0))
|
||||||
|
return "synth-domain domain and address range cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(tokens[0]))
|
||||||
|
return "synth-domain domain must be a valid DNS name.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic validation for <c>txt-record</c> values: <name>[[,<text>],<text>].
|
||||||
|
/// Name required (first comma-separated field); optional text strings (commas allowed in quoted strings).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TxtRecordSemanticHandler : IOptionSemanticHandler
|
||||||
|
{
|
||||||
|
public bool CanHandle(string optionName) => optionName == DnsmasqConfKeys.TxtRecord;
|
||||||
|
|
||||||
|
public string? ValidateSingle(object? value) => null;
|
||||||
|
|
||||||
|
public string? ValidateMultiItem(string? value)
|
||||||
|
{
|
||||||
|
var s = (value ?? "").Trim();
|
||||||
|
if (s.Length == 0)
|
||||||
|
return "txt-record value cannot be empty.";
|
||||||
|
var name = s.Contains(',') ? s.Split(',', 2, StringSplitOptions.TrimEntries)[0].Trim() : s;
|
||||||
|
if (name.Length == 0)
|
||||||
|
return "txt-record name cannot be empty.";
|
||||||
|
if (!DnsmasqScopedDomainSyntax.IsValidDnsName(name))
|
||||||
|
return "txt-record name must be a valid DNS name.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user