2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-09-02 07:15:24 +00:00

Adding NAPTR support (#594)

* Adding NAPTR support
This commit is contained in:
Thomas Pressnell
2019-04-24 16:22:05 +01:00
committed by Paul Cleary
parent 5040d07c00
commit 77c4536e37
22 changed files with 542 additions and 7 deletions

View File

@@ -64,6 +64,82 @@ def test_create_recordset_with_dns_verify(shared_zone_test_context):
pass pass
def test_create_naptr_origin_record(shared_zone_test_context):
"""
Test creating naptr origin records works
"""
client = shared_zone_test_context.ok_vinyldns_client
result_rs = None
try:
new_rs = {
'zoneId': shared_zone_test_context.ok_zone['id'],
'name': 'ok.',
'type': 'NAPTR',
'ttl': 100,
'records': [
{
'order': 10,
'preference': 100,
'flags': 'S',
'service': 'SIP+D2T',
'regexp': '',
'replacement': '_sip._udp.ok.'
}
]
}
result = client.create_recordset(new_rs, status=202)
assert_that(result['changeType'], is_('Create'))
assert_that(result['status'], is_('Pending'))
assert_that(result['created'], is_not(none()))
assert_that(result['userId'], is_not(none()))
result_rs = result['recordSet']
result_rs = client.wait_until_recordset_change_status(result, 'Complete')['recordSet']
verify_recordset(result_rs, new_rs)
finally:
if result_rs:
delete_result = client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
def test_create_naptr_non_origin_record(shared_zone_test_context):
"""
Test creating naptr records works
"""
client = shared_zone_test_context.ok_vinyldns_client
result_rs = None
try:
new_rs = {
'zoneId': shared_zone_test_context.ok_zone['id'],
'name': 'testnaptr',
'type': 'NAPTR',
'ttl': 100,
'records': [
{
'order': 10,
'preference': 100,
'flags': 'S',
'service': 'SIP+D2T',
'regexp': '',
'replacement': '_sip._udp.ok.'
}
]
}
result = client.create_recordset(new_rs, status=202)
assert_that(result['changeType'], is_('Create'))
assert_that(result['status'], is_('Pending'))
assert_that(result['created'], is_not(none()))
assert_that(result['userId'], is_not(none()))
result_rs = result['recordSet']
result_rs = client.wait_until_recordset_change_status(result, 'Complete')['recordSet']
verify_recordset(result_rs, new_rs)
finally:
if result_rs:
delete_result = client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
def test_create_srv_recordset_with_service_and_protocol(shared_zone_test_context): def test_create_srv_recordset_with_service_and_protocol(shared_zone_test_context):
""" """
Test creating a new srv record set with service and protocol works Test creating a new srv record set with service and protocol works

View File

@@ -133,6 +133,7 @@ trait DnsConversions {
case DNS.Type.SOA => RecordType.SOA case DNS.Type.SOA => RecordType.SOA
case DNS.Type.SPF => RecordType.SPF case DNS.Type.SPF => RecordType.SPF
case DNS.Type.SRV => RecordType.SRV case DNS.Type.SRV => RecordType.SRV
case DNS.Type.NAPTR => RecordType.NAPTR
case DNS.Type.SSHFP => RecordType.SSHFP case DNS.Type.SSHFP => RecordType.SSHFP
case DNS.Type.TXT => RecordType.TXT case DNS.Type.TXT => RecordType.TXT
case _ => RecordType.UNKNOWN case _ => RecordType.UNKNOWN
@@ -150,6 +151,7 @@ trait DnsConversions {
case x: DNS.SOARecord => fromSOARecord(x, zoneName, zoneId) case x: DNS.SOARecord => fromSOARecord(x, zoneName, zoneId)
case x: DNS.SPFRecord => fromSPFRecord(x, zoneName, zoneId) case x: DNS.SPFRecord => fromSPFRecord(x, zoneName, zoneId)
case x: DNS.SRVRecord => fromSRVRecord(x, zoneName, zoneId) case x: DNS.SRVRecord => fromSRVRecord(x, zoneName, zoneId)
case x: DNS.NAPTRRecord => fromNAPTRRecord(x, zoneName, zoneId)
case x: DNS.SSHFPRecord => fromSSHFPRecord(x, zoneName, zoneId) case x: DNS.SSHFPRecord => fromSSHFPRecord(x, zoneName, zoneId)
case x: DNS.TXTRecord => fromTXTRecord(x, zoneName, zoneId) case x: DNS.TXTRecord => fromTXTRecord(x, zoneName, zoneId)
case _ => fromUnknownRecordType(r, zoneName, zoneId) case _ => fromUnknownRecordType(r, zoneName, zoneId)
@@ -267,6 +269,18 @@ trait DnsConversions {
List(SRVData(data.getPriority, data.getWeight, data.getPort, data.getTarget.toString)) List(SRVData(data.getPriority, data.getWeight, data.getPort, data.getTarget.toString))
} }
def fromNAPTRRecord(r: DNS.NAPTRRecord, zoneName: DNS.Name, zoneId: String): RecordSet =
fromDnsRecord(r, zoneName, zoneId) { data =>
List(
NAPTRData(
data.getOrder,
data.getPreference,
data.getFlags,
data.getService,
data.getRegexp,
data.getReplacement.toString))
}
def fromSSHFPRecord(r: DNS.SSHFPRecord, zoneName: DNS.Name, zoneId: String): RecordSet = def fromSSHFPRecord(r: DNS.SSHFPRecord, zoneName: DNS.Name, zoneId: String): RecordSet =
fromDnsRecord(r, zoneName, zoneId) { data => fromDnsRecord(r, zoneName, zoneId) { data =>
List(SSHFPData(data.getAlgorithm, data.getDigestType, new String(data.getFingerPrint))) List(SSHFPData(data.getAlgorithm, data.getDigestType, new String(data.getFingerPrint)))
@@ -339,6 +353,18 @@ trait DnsConversions {
port, port,
DNS.Name.fromString(target)) DNS.Name.fromString(target))
case NAPTRData(order, preference, flags, service, regexp, replacement) =>
new DNS.NAPTRRecord(
recordName,
DNS.DClass.IN,
ttl,
order,
preference,
flags,
service,
regexp,
DNS.Name.fromString(replacement))
case SSHFPData(algorithm, typ, fingerprint) => case SSHFPData(algorithm, typ, fingerprint) =>
new DNS.SSHFPRecord(recordName, DNS.DClass.IN, ttl, algorithm, typ, fingerprint.getBytes) new DNS.SSHFPRecord(recordName, DNS.DClass.IN, ttl, algorithm, typ, fingerprint.getBytes)
@@ -371,6 +397,7 @@ trait DnsConversions {
case RecordType.SPF => DNS.Type.SPF case RecordType.SPF => DNS.Type.SPF
case RecordType.SSHFP => DNS.Type.SSHFP case RecordType.SSHFP => DNS.Type.SSHFP
case RecordType.SRV => DNS.Type.SRV case RecordType.SRV => DNS.Type.SRV
case RecordType.NAPTR => DNS.Type.NAPTR
case RecordType.TXT => DNS.Type.TXT case RecordType.TXT => DNS.Type.TXT
} }

View File

@@ -92,7 +92,7 @@ object RecordSetValidations {
case NS => nsValidations(newRecordSet, zone) case NS => nsValidations(newRecordSet, zone)
case SOA => soaValidations(newRecordSet, zone) case SOA => soaValidations(newRecordSet, zone)
case PTR => ptrValidations(newRecordSet, zone) case PTR => ptrValidations(newRecordSet, zone)
case SRV | TXT => ().asRight // SRV and TXT do not go through dotted host check case SRV | TXT | NAPTR => ().asRight // SRV, TXT and NAPTR do not go through dotted host check
case DS => dsValidations(newRecordSet, existingRecordsWithName, zone) case DS => dsValidations(newRecordSet, existingRecordsWithName, zone)
case _ => isNotDotted(newRecordSet, zone) case _ => isNotDotted(newRecordSet, zone)
} }
@@ -107,7 +107,7 @@ object RecordSetValidations {
case NS => nsValidations(newRecordSet, zone, Some(oldRecordSet)) case NS => nsValidations(newRecordSet, zone, Some(oldRecordSet))
case SOA => soaValidations(newRecordSet, zone) case SOA => soaValidations(newRecordSet, zone)
case PTR => ptrValidations(newRecordSet, zone) case PTR => ptrValidations(newRecordSet, zone)
case SRV | TXT => ().asRight // SRV and TXT do not go through dotted host check case SRV | TXT | NAPTR => ().asRight // SRV, TXT and NAPTR do not go through dotted host check
case DS => dsValidations(newRecordSet, existingRecordsWithName, zone) case DS => dsValidations(newRecordSet, existingRecordsWithName, zone)
case _ => isNotDotted(newRecordSet, zone) case _ => isNotDotted(newRecordSet, zone)
} }

View File

@@ -106,7 +106,8 @@ object ZoneSyncHandler extends DnsConversions with Monitored {
changesWithUserIds changesWithUserIds
.filter { chg => .filter { chg =>
chg.recordSet.name != zone.name && chg.recordSet.name.contains(".") && chg.recordSet.name != zone.name && chg.recordSet.name.contains(".") &&
chg.recordSet.typ != RecordType.SRV && chg.recordSet.typ != RecordType.TXT chg.recordSet.typ != RecordType.SRV && chg.recordSet.typ != RecordType.TXT &&
chg.recordSet.typ != RecordType.NAPTR
} }
.map(_.recordSet.name) .map(_.recordSet.name)
.grouped(1000) .grouped(1000)

View File

@@ -57,6 +57,7 @@ trait DnsJsonProtocol extends JsonValidation {
SOASerializer, SOASerializer,
SPFSerializer, SPFSerializer,
SRVSerializer, SRVSerializer,
NAPTRSerializer,
SSHFPSerializer, SSHFPSerializer,
TXTSerializer, TXTSerializer,
JsonV[ZoneACL] JsonV[ZoneACL]
@@ -263,6 +264,7 @@ trait DnsJsonProtocol extends JsonValidation {
case RecordType.SOA => js.required[List[SOAData]]("Missing SOA Records") case RecordType.SOA => js.required[List[SOAData]]("Missing SOA Records")
case RecordType.SPF => js.required[List[SPFData]]("Missing SPF Records") case RecordType.SPF => js.required[List[SPFData]]("Missing SPF Records")
case RecordType.SRV => js.required[List[SRVData]]("Missing SRV Records") case RecordType.SRV => js.required[List[SRVData]]("Missing SRV Records")
case RecordType.NAPTR => js.required[List[NAPTRData]]("Missing NAPTR Records")
case RecordType.SSHFP => js.required[List[SSHFPData]]("Missing SSHFP Records") case RecordType.SSHFP => js.required[List[SSHFPData]]("Missing SSHFP Records")
case RecordType.TXT => js.required[List[TXTData]]("Missing TXT Records") case RecordType.TXT => js.required[List[TXTData]]("Missing TXT Records")
case _ => s"Unsupported type $typ, valid types include ${RecordType.values}".invalidNel case _ => s"Unsupported type $typ, valid types include ${RecordType.values}".invalidNel
@@ -417,6 +419,44 @@ trait DnsJsonProtocol extends JsonValidation {
).mapN(SRVData.apply) ).mapN(SRVData.apply)
} }
case object NAPTRSerializer extends ValidationSerializer[NAPTRData] {
override def fromJson(js: JValue): ValidatedNel[String, NAPTRData] =
(
(js \ "order")
.required[Integer]("Missing NAPTR.order")
.check(
"NAPTR.order must be an unsigned 16 bit number" -> (i => i <= 65535 && i >= 0)
),
(js \ "preference")
.required[Integer]("Missing NAPTR.preference")
.check(
"NAPTR.preference must be an unsigned 16 bit number" -> (i => i <= 65535 && i >= 0)
),
(js \ "flags")
.required[String]("Missing NAPTR.flags")
.check(
"NAPTR.flags must be less than 2 characters" -> (_.length < 2)
),
(js \ "service")
.required[String]("Missing NAPTR.service")
.check(
"NAPTR.service must be less than 255 characters" -> checkDomainNameLen
),
(js \ "regexp")
.required[String]("Missing NAPTR.regexp")
.check(
"NAPTR.regexp must be less than 255 characters" -> checkDomainNameLen
),
// should also check regex validity
(js \ "replacement")
.required[String]("Missing NAPTR.replacement")
.check(
"NAPTR.replacement must be less than 255 characters" -> checkDomainNameLen
)
.map(ensureTrailingDot)
).mapN(NAPTRData.apply)
}
case object SSHFPSerializer extends ValidationSerializer[SSHFPData] { case object SSHFPSerializer extends ValidationSerializer[SSHFPData] {
override def fromJson(js: JValue): ValidatedNel[String, SSHFPData] = override def fromJson(js: JValue): ValidatedNel[String, SSHFPData] =
( (

View File

@@ -142,6 +142,16 @@ class DnsConversionsSpec
DateTime.now, DateTime.now,
None, None,
List(SRVData(1, 2, 3, "target.vinyldns."))) List(SRVData(1, 2, 3, "target.vinyldns.")))
private val testNAPTR = RecordSet(
testZone.id,
"naptr-record",
RecordType.NAPTR,
200,
RecordSetStatus.Active,
DateTime.now,
None,
List(NAPTRData(1, 2, "U", "E2U+sip", "!.*!test.!", "target.vinyldns."))
)
private val testSSHFP = RecordSet( private val testSSHFP = RecordSet(
testZone.id, testZone.id,
"sshfp-record", "sshfp-record",
@@ -350,6 +360,11 @@ class DnsConversionsSpec
verifyMatch(result, testSRV) verifyMatch(result, testSRV)
} }
"convert NAPTR record set" in {
val result = rightValue(toDnsRRset(testNAPTR, testZoneName))
verifyMatch(result, testNAPTR)
}
"convert TXT record set" in { "convert TXT record set" in {
val result = rightValue(toDnsRRset(testTXT, testZoneName)) val result = rightValue(toDnsRRset(testTXT, testZoneName))
verifyMatch(result, testTXT) verifyMatch(result, testTXT)
@@ -464,6 +479,9 @@ class DnsConversionsSpec
"convert to/from RecordType SRV" in { "convert to/from RecordType SRV" in {
verifyMatch(testSRV, roundTrip(testSRV)) verifyMatch(testSRV, roundTrip(testSRV))
} }
"convert to/from RecordType NAPTR" in {
verifyMatch(testNAPTR, roundTrip(testNAPTR))
}
"convert to/from RecordType SSHFP" in { "convert to/from RecordType SSHFP" in {
verifyMatch(testSSHFP, roundTrip(testSSHFP)) verifyMatch(testSSHFP, roundTrip(testSSHFP))
} }
@@ -509,6 +527,9 @@ class DnsConversionsSpec
"support SRV" in { "support SRV" in {
toDnsRecordType(RecordType.SRV) shouldBe DNS.Type.SRV toDnsRecordType(RecordType.SRV) shouldBe DNS.Type.SRV
} }
"support NAPTR" in {
toDnsRecordType(RecordType.NAPTR) shouldBe DNS.Type.NAPTR
}
"support TXT" in { "support TXT" in {
toDnsRecordType(RecordType.TXT) shouldBe DNS.Type.TXT toDnsRecordType(RecordType.TXT) shouldBe DNS.Type.TXT
} }

View File

@@ -55,11 +55,21 @@ class RecordSetValidationsSpec
error shouldBe an[InvalidRequest] error shouldBe an[InvalidRequest]
} }
"return invalid request when adding a NAPTR record to an IP4 reverse zone" in {
val error = leftValue(validRecordTypes(naptr, zoneIp4))
error shouldBe an[InvalidRequest]
}
"return invalid request when adding a SRV record to an IP6 reverse zone" in { "return invalid request when adding a SRV record to an IP6 reverse zone" in {
val error = leftValue(validRecordTypes(srv, zoneIp6)) val error = leftValue(validRecordTypes(srv, zoneIp6))
error shouldBe an[InvalidRequest] error shouldBe an[InvalidRequest]
} }
"return invalid request when adding a NAPTR record to an IP6 reverse zone" in {
val error = leftValue(validRecordTypes(naptr, zoneIp6))
error shouldBe an[InvalidRequest]
}
"return ok when adding an acceptable record to a forward zone" in { "return ok when adding an acceptable record to a forward zone" in {
validRecordTypes(aaaa, okZone) should be(right) validRecordTypes(aaaa, okZone) should be(right)
} }
@@ -214,6 +224,29 @@ class RecordSetValidationsSpec
typeSpecificAddValidations(test, List(), zone) should be(right) typeSpecificAddValidations(test, List(), zone) should be(right)
} }
} }
"Skip dotted checks on NAPTR" should {
"return success for an NAPTR record with FQDN" in {
val test = naptr.copy(name = "sub.naptr.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificAddValidations(test, List(), zone) should be(right)
}
"return success for an NAPTR record without FQDN" in {
val test = naptr.copy(name = "sub.naptr")
val zone = okZone.copy(name = "example.com.")
typeSpecificAddValidations(test, List(), zone) should be(right)
}
"return success on a wildcard NAPTR" in {
val test = naptr.copy(name = "*.sub.naptr.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificAddValidations(test, List(), zone) should be(right)
}
}
"Skip dotted checks on PTR" should { "Skip dotted checks on PTR" should {
"return success for a PTR record with dots in a reverse zone" in { "return success for a PTR record with dots in a reverse zone" in {
val test = ptrIp4.copy(name = "10.1.2.") val test = ptrIp4.copy(name = "10.1.2.")

View File

@@ -154,6 +154,15 @@ class ProtobufConversionsSpec
DateTime.now, DateTime.now,
None, None,
List(SRVData(1, 2, 3, "target"))) List(SRVData(1, 2, 3, "target")))
private val naptr = RecordSet(
zone.id,
"naptr",
RecordType.NAPTR,
200,
RecordSetStatus.Active,
DateTime.now,
None,
List(NAPTRData(1, 2, "U", "E2U+sip", "!.*!test.!", "target")))
private val sshfp = RecordSet( private val sshfp = RecordSet(
zone.id, zone.id,
"sshfp", "sshfp",
@@ -527,6 +536,10 @@ class ProtobufConversionsSpec
fromPB(toPB(srv)) shouldBe srv fromPB(toPB(srv)) shouldBe srv
} }
"convert from protobuf for NAPTR recordset" in {
fromPB(toPB(naptr)) shouldBe naptr
}
"convert from protobuf for SSHFP recordset" in { "convert from protobuf for SSHFP recordset" in {
fromPB(toPB(sshfp)) shouldBe sshfp fromPB(toPB(sshfp)) shouldBe sshfp
} }
@@ -665,6 +678,25 @@ class ProtobufConversionsSpec
data shouldBe from data shouldBe from
} }
"convert to protobuf for NAPTR data" in {
val from = NAPTRData(1, 2, "U", "E2U+sip", "!.*!test.!", "target")
val pb = toPB(from)
pb.getOrder shouldBe from.order
pb.getPreference shouldBe from.preference
pb.getFlags shouldBe from.flags
pb.getService shouldBe from.service
pb.getRegexp shouldBe from.regexp
pb.getReplacement shouldBe from.replacement
}
"convert from protobuf for NAPTR data" in {
val from = NAPTRData(1, 2, "U", "E2U+sip", "!.*!test.!", "target")
val pb = toPB(from)
val data = fromPB(pb)
data shouldBe from
}
"convert to protobuf for SSHFP data" in { "convert to protobuf for SSHFP data" in {
val from = SSHFPData(1, 2, "fingerprint") val from = SSHFPData(1, 2, "fingerprint")
val pb = toPB(from) val pb = toPB(from)

View File

@@ -261,6 +261,16 @@ class RecordSetRoutingSpec
None, None,
List(SRVData(1, 2, 3, "target."))) List(SRVData(1, 2, 3, "target.")))
private val naptr = RecordSet(
okZone.id,
"naptr",
RecordType.NAPTR,
200,
RecordSetStatus.Active,
DateTime.now,
None,
List(NAPTRData(1, 2, "U", "E2U+sip", "!.*!test.!", "target.")))
private val sshfp = RecordSet( private val sshfp = RecordSet(
okZone.id, okZone.id,
"sshfp", "sshfp",
@@ -325,6 +335,7 @@ class RecordSetRoutingSpec
soa.id -> soa, soa.id -> soa,
spf.id -> spf, spf.id -> spf,
srv.id -> srv, srv.id -> srv,
naptr.id -> naptr,
sshfp.id -> sshfp, sshfp.id -> sshfp,
txt.id -> txt txt.id -> txt
) )
@@ -908,6 +919,7 @@ class RecordSetRoutingSpec
validateErrors(testRecordBase(RecordType.SOA, JNothing), "Missing SOA Records") validateErrors(testRecordBase(RecordType.SOA, JNothing), "Missing SOA Records")
validateErrors(testRecordBase(RecordType.SPF, JNothing), "Missing SPF Records") validateErrors(testRecordBase(RecordType.SPF, JNothing), "Missing SPF Records")
validateErrors(testRecordBase(RecordType.SRV, JNothing), "Missing SRV Records") validateErrors(testRecordBase(RecordType.SRV, JNothing), "Missing SRV Records")
validateErrors(testRecordBase(RecordType.NAPTR, JNothing), "Missing NAPTR Records")
validateErrors(testRecordBase(RecordType.SSHFP, JNothing), "Missing SSHFP Records") validateErrors(testRecordBase(RecordType.SSHFP, JNothing), "Missing SSHFP Records")
validateErrors(testRecordBase(RecordType.TXT, JNothing), "Missing TXT Records") validateErrors(testRecordBase(RecordType.TXT, JNothing), "Missing TXT Records")
} }
@@ -1124,6 +1136,56 @@ class RecordSetRoutingSpec
) )
} }
"supports NAPTR" in {
validateCreateRecordType(naptr)
}
"return errors for NAPTR record missing data" in {
validateErrors(
testRecordType(RecordType.NAPTR, "key" -> "val"),
"Missing NAPTR.order",
"Missing NAPTR.preference",
"Missing NAPTR.flags",
"Missing NAPTR.service",
"Missing NAPTR.regexp",
"Missing NAPTR.replacement"
)
}
"return errors for invalid NAPTR record data" in {
validateErrors(
testRecordType(
RecordType.NAPTR,
("replacement" -> Random.alphanumeric.take(260).mkString) ~~
// should check regex better
("regexp" -> Random.alphanumeric.take(260).mkString) ~~
("service" -> Random.alphanumeric.take(260).mkString) ~~
("flags" -> Random.alphanumeric.take(2).mkString) ~~
("order" -> 50000000) ~~
("preference" -> 50000000)
),
"NAPTR.order must be an unsigned 16 bit number",
"NAPTR.preference must be an unsigned 16 bit number",
"NAPTR.flags must be less than 2 characters",
"NAPTR.service must be less than 255 characters",
"NAPTR.regexp must be less than 255 characters",
"NAPTR.replacement must be less than 255 characters"
)
validateErrors(
testRecordType(
RecordType.NAPTR,
("regexp" -> Random.alphanumeric.take(10).mkString) ~~
("service" -> Random.alphanumeric.take(10).mkString) ~~
("replacement" -> Random.alphanumeric.take(10).mkString) ~~
("flags" -> Random.alphanumeric.take(1).mkString) ~~
("order" -> -1) ~~
("preference" -> -1)
),
"NAPTR.order must be an unsigned 16 bit number",
"NAPTR.preference must be an unsigned 16 bit number"
)
}
"supports SSHFP" in { "supports SSHFP" in {
validateCreateRecordType(sshfp) validateCreateRecordType(sshfp)
} }

View File

@@ -444,6 +444,55 @@ class VinylDNSJsonProtocolSpec
anonymize(actual).records shouldBe List(SRVData(1, 20, 5000, "srv.")) anonymize(actual).records shouldBe List(SRVData(1, 20, 5000, "srv."))
} }
"parse a record set with an absolute NAPTR target passes" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "NAPTR") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> Extraction.decompose(
Set(NAPTRData(1, 20, "U", "E2U+sip", "!.*!test.!", "naptr."))))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.NAPTR,
1000,
RecordSetStatus.Pending,
new DateTime(2010, 1, 1, 0, 0),
records = List(NAPTRData(1, 20, "U", "E2U+sip", "!.*!test.!", "naptr."))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
}
"convert relative NAPTR target to an absolute NAPTR target" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "NAPTR") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> Extraction.decompose(
Set(NAPTRData(1, 20, "U", "E2U+sip", "!.*!test.!", "naptr"))))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.NAPTR,
1000,
RecordSetStatus.Pending,
new DateTime(2010, 1, 1, 0, 0),
records = List(NAPTRData(1, 20, "U", "E2U+sip", "!.*!test.!", "naptr."))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).records shouldBe List(
NAPTRData(1, 20, "U", "E2U+sip", "!.*!test.!", "naptr."))
}
"parse a record set with an absolute PTR domain name" in { "parse a record set with an absolute PTR domain name" in {
val recordSetJValue: JValue = val recordSetJValue: JValue =
("zoneId" -> "1") ~~ ("zoneId" -> "1") ~~

View File

@@ -94,6 +94,15 @@ message SRVData {
required string target = 4; required string target = 4;
} }
message NAPTRData {
required int32 order = 1;
required int32 preference = 2;
required string flags = 4;
required string service = 5;
required string regexp = 6;
required string replacement = 7;
}
message SSHFPData { message SSHFPData {
required int32 algorithm = 1; required int32 algorithm = 1;
required int32 typ = 2; required int32 typ = 2;

View File

@@ -73,6 +73,26 @@ object SRVData {
new SRVData(priority, weight, port, ensureTrailingDot(target)) new SRVData(priority, weight, port, ensureTrailingDot(target))
} }
final case class NAPTRData(
order: Integer,
preference: Integer,
flags: String,
service: String,
regexp: String,
replacement: String)
extends RecordData
object NAPTRData {
def apply(
order: Integer,
preference: Integer,
flags: String,
service: String,
regexp: String,
replacement: String): NAPTRData =
new NAPTRData(order, preference, flags, service, regexp, ensureTrailingDot(replacement))
}
final case class SSHFPData(algorithm: Integer, typ: Integer, fingerprint: String) extends RecordData final case class SSHFPData(algorithm: Integer, typ: Integer, fingerprint: String) extends RecordData
final case class TXTData(text: String) extends RecordData final case class TXTData(text: String) extends RecordData

View File

@@ -22,7 +22,7 @@ import org.joda.time.DateTime
object RecordType extends Enumeration { object RecordType extends Enumeration {
type RecordType = Value type RecordType = Value
val A, AAAA, CNAME, DS, PTR, MX, NS, SOA, SRV, TXT, SSHFP, SPF, UNKNOWN = Value val A, AAAA, CNAME, DS, PTR, MX, NS, SOA, SRV, NAPTR, TXT, SSHFP, SPF, UNKNOWN = Value
} }
object RecordSetStatus extends Enumeration { object RecordSetStatus extends Enumeration {

View File

@@ -165,6 +165,7 @@ trait ProtobufConversions {
case RecordType.SOA => fromPB(VinylDNSProto.SOAData.parseFrom(rd.getData)) case RecordType.SOA => fromPB(VinylDNSProto.SOAData.parseFrom(rd.getData))
case RecordType.SPF => fromPB(VinylDNSProto.SPFData.parseFrom(rd.getData)) case RecordType.SPF => fromPB(VinylDNSProto.SPFData.parseFrom(rd.getData))
case RecordType.SRV => fromPB(VinylDNSProto.SRVData.parseFrom(rd.getData)) case RecordType.SRV => fromPB(VinylDNSProto.SRVData.parseFrom(rd.getData))
case RecordType.NAPTR => fromPB(VinylDNSProto.NAPTRData.parseFrom(rd.getData))
case RecordType.SSHFP => fromPB(VinylDNSProto.SSHFPData.parseFrom(rd.getData)) case RecordType.SSHFP => fromPB(VinylDNSProto.SSHFPData.parseFrom(rd.getData))
case RecordType.TXT => fromPB(VinylDNSProto.TXTData.parseFrom(rd.getData)) case RecordType.TXT => fromPB(VinylDNSProto.TXTData.parseFrom(rd.getData))
} }
@@ -203,6 +204,15 @@ trait ProtobufConversions {
def fromPB(data: VinylDNSProto.SRVData): SRVData = def fromPB(data: VinylDNSProto.SRVData): SRVData =
SRVData(data.getPriority, data.getWeight, data.getPort, data.getTarget) SRVData(data.getPriority, data.getWeight, data.getPort, data.getTarget)
def fromPB(data: VinylDNSProto.NAPTRData): NAPTRData =
NAPTRData(
data.getOrder,
data.getPreference,
data.getFlags,
data.getService,
data.getRegexp,
data.getReplacement)
def fromPB(data: VinylDNSProto.SSHFPData): SSHFPData = def fromPB(data: VinylDNSProto.SSHFPData): SSHFPData =
SSHFPData(data.getAlgorithm, data.getTyp, data.getFingerPrint) SSHFPData(data.getAlgorithm, data.getTyp, data.getFingerPrint)
@@ -283,6 +293,17 @@ trait ProtobufConversions {
.setWeight(data.weight) .setWeight(data.weight)
.build() .build()
def toPB(data: NAPTRData): VinylDNSProto.NAPTRData =
VinylDNSProto.NAPTRData
.newBuilder()
.setOrder(data.order)
.setPreference(data.preference)
.setFlags(data.flags)
.setService(data.service)
.setRegexp(data.regexp)
.setReplacement(data.replacement)
.build()
def toPB(data: SSHFPData): VinylDNSProto.SSHFPData = def toPB(data: SSHFPData): VinylDNSProto.SSHFPData =
VinylDNSProto.SSHFPData VinylDNSProto.SSHFPData
.newBuilder() .newBuilder()
@@ -307,6 +328,7 @@ trait ProtobufConversions {
case x: SOAData => toPB(x) case x: SOAData => toPB(x)
case x: SPFData => toPB(x) case x: SPFData => toPB(x)
case x: SRVData => toPB(x) case x: SRVData => toPB(x)
case x: NAPTRData => toPB(x)
case x: SSHFPData => toPB(x) case x: SSHFPData => toPB(x)
case x: TXTData => toPB(x) case x: TXTData => toPB(x)
} }

View File

@@ -108,6 +108,16 @@ object TestRecordSetData {
None, None,
List(SRVData(1, 2, 3, "target"))) List(SRVData(1, 2, 3, "target")))
val naptr: RecordSet = RecordSet(
okZone.id,
"naptr",
RecordType.NAPTR,
200,
RecordSetStatus.Active,
DateTime.now,
None,
List(NAPTRData(1, 2, "S", "E2U+sip", "", "target")))
val mx: RecordSet = RecordSet( val mx: RecordSet = RecordSet(
okZone.id, okZone.id,
"mx", "mx",

View File

@@ -63,5 +63,11 @@ class RecordSetSpec extends WordSpec with Matchers {
result.records shouldBe List(SRVData(1, 2, 3, "target.")) result.records shouldBe List(SRVData(1, 2, 3, "target."))
} }
"ensure trailing dot on NAPTR record target" in {
val result = naptr
result.records shouldBe List(NAPTRData(1, 2, "S", "E2U+sip", "", "target."))
}
} }
} }

View File

@@ -318,6 +318,7 @@ object MySqlRecordSetRepository extends ProtobufConversions {
case RecordType.TXT => 10 case RecordType.TXT => 10
case RecordType.SOA => 11 case RecordType.SOA => 11
case RecordType.DS => 12 case RecordType.DS => 12
case RecordType.NAPTR => 13
case RecordType.UNKNOWN => unknownRecordType case RecordType.UNKNOWN => unknownRecordType
} }

View File

@@ -265,6 +265,21 @@
</span> </span>
</ul> </ul>
</span> </span>
<span ng-if="record.type == 'NAPTR'">
<ul class="table-cell-list">
<li ng-repeat="item in record.naptrItems track by $index"
ng-if="!record.onlyFour || $index <= 3">
Order: {{ item.order }} | Preference: {{ item.preference }} | Flags: {{ item.flags }}
| Service: {{ item.service }} | Regexp: {{ item.regexp }} | Replacement: {{ item.replacement }}
</li>
<li ng-if="record.onlyFour && record.naptrItems.length > 4">
<a href ng-click="record.onlyFour=false">more...</a>
</li>
<li ng-if="!record.onlyFour">
<a href ng-click="record.onlyFour=true">less...</a>
</li>
</ul>
</span>
<span ng-if="record.type == 'SSHFP'"> <span ng-if="record.type == 'SSHFP'">
<ul class="table-cell-list"> <ul class="table-cell-list">
<li ng-repeat="item in record.sshfpItems track by $index" <li ng-repeat="item in record.sshfpItems track by $index"

View File

@@ -63,7 +63,7 @@ angular.module('controller.manageZones', [])
readOnly: false readOnly: false
} }
}; };
$scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT']; $scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SPF', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
/** /**
* Zone modal control functions * Zone modal control functions

View File

@@ -24,7 +24,7 @@ angular.module('controller.records', [])
$scope.query = ""; $scope.query = "";
$scope.alerts = []; $scope.alerts = [];
$scope.recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT']; $scope.recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
$scope.sshfpAlgorithms = [{name: '(1) RSA', number: 1}, {name: '(2) DSA', number: 2}, {name: '(3) ECDSA', number: 3}, $scope.sshfpAlgorithms = [{name: '(1) RSA', number: 1}, {name: '(2) DSA', number: 2}, {name: '(3) ECDSA', number: 3},
{name: '(4) Ed25519', number: 4}]; {name: '(4) Ed25519', number: 4}];
$scope.sshfpTypes = [{name: '(1) SHA-1', number: 1}, {name: '(2) SHA-256', number: 2}]; $scope.sshfpTypes = [{name: '(1) SHA-1', number: 1}, {name: '(2) SHA-256', number: 2}];
@@ -92,6 +92,7 @@ angular.module('controller.records', [])
ttl: 300, ttl: 300,
mxItems: [{preference:'', exchange:''}], mxItems: [{preference:'', exchange:''}],
srvItems: [{priority:'', weight:'', port:'', target:''}], srvItems: [{priority:'', weight:'', port:'', target:''}],
naptrItems: [{order:'', preference:'', flags:'', service:'', regexp:'', replacement:''}],
sshfpItems: [{algorithm:'', type:'', fingerprint:''}] sshfpItems: [{algorithm:'', type:'', fingerprint:''}]
}; };
$scope.currentRecord = angular.copy(record); $scope.currentRecord = angular.copy(record);
@@ -239,6 +240,18 @@ angular.module('controller.records', [])
} }
}; };
$scope.addNewNaptr = function() {
var dataObj = {order:'', preference:'', flags:'', service:'', regexp:'', replacement:''};
$scope.currentRecord.naptrItems.push(dataObj);
};
$scope.deleteNaptr = function(index) {
$scope.currentRecord.naptrItems.splice(index, 1);
if($scope.currentRecord.naptrItems.length == 0) {
$scope.addNewNaptr();
}
};
$scope.addNewSshfp = function() { $scope.addNewSshfp = function() {
var dataObj = {algorithm:'', type:'', fingerprint: ''}; var dataObj = {algorithm:'', type:'', fingerprint: ''};
$scope.currentRecord.sshfpItems.push(dataObj); $scope.currentRecord.sshfpItems.push(dataObj);

View File

@@ -76,7 +76,7 @@ angular.module('service.records', [])
} }
function isDotted(record, zoneName) { function isDotted(record, zoneName) {
var canHaveDots = ['PTR', 'NS', 'SOA', 'SRV']; var canHaveDots = ['PTR', 'NS', 'SOA', 'SRV', 'NAPTR'];
return canHaveDots.indexOf(record.type) == -1 && return canHaveDots.indexOf(record.type) == -1 &&
record.name.indexOf(".") != -1 && record.name.indexOf(".") != -1 &&
@@ -163,6 +163,13 @@ angular.module('service.records', [])
}); });
newRecord.onlyFour = true; newRecord.onlyFour = true;
break; break;
case 'NAPTR':
newRecord.naptrItems = [];
angular.forEach(record.records, function(item) {
newRecord.naptrItems.push(item);
});
newRecord.onlyFour = true;
break;
case 'SSHFP': case 'SSHFP':
newRecord.sshfpItems = []; newRecord.sshfpItems = [];
angular.forEach(record.records, function(item) { angular.forEach(record.records, function(item) {
@@ -233,6 +240,17 @@ angular.module('service.records', [])
"target": record.target}); "target": record.target});
}); });
break; break;
case 'NAPTR':
newRecord.records = [];
angular.forEach(record.naptrItems, function(record) {
newRecord.records.push({"order": Number(record.order),
"preference": Number(record.preference),
"flags": record.flags,
"service": record.service,
"regexp": record.regexp,
"replacement": record.replacement});
});
break;
case 'SPF': case 'SPF':
newRecord.records = []; newRecord.records = [];
angular.forEach(record.spfRecordData, function(text) { angular.forEach(record.spfRecordData, function(text) {

View File

@@ -248,6 +248,86 @@
</button> </button>
</modal-element> </modal-element>
<modal-element ng-if="currentRecord.type == 'NAPTR'" label="Record Data">
<table class="table table-condensed">
<tr>
<td class="table-col-10"><label class="control-label">Order</label></td>
<td class="table-col-10"><label class="control-label">Preference</label></td>
<td class="table-col-10"><label class="control-label">Flags</label></td>
<td class="table-col-20"><label class="control-label">Service</label></td>
<td class="table-col-25"><label class="control-label">Regexp</label></td>
<td class="table-col-25"><label class="control-label">Replacement</label></td>
<td></td>
</tr>
<tr ng-repeat="item in currentRecord.naptrItems">
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['order_' + ($index)].$invalid}">
<input name="order_{{$index}}"
class="form-control"
ng-model="item.order"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly"
required>
</input>
</td>
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['preference_' + ($index)].$invalid}">
<input name="weight_{{$index}}"
class="form-control"
ng-model="item.preference"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly"
required>
</input>
</td>
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['flags_' + ($index)].$invalid}">
<input name="flags_{{$index}}"
class="form-control"
ng-model="item.flags"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly"
required>
</input>
</td>
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['service_' + ($index)].$invalid}">
<input name="flags_{{$index}}"
class="form-control"
ng-model="item.service"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly"
required>
</input>
</td>
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['regexp_' + ($index)].$invalid}">
<input name="flags_{{$index}}"
class="form-control"
ng-model="item.regexp"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly">
</input>
</td>
<td ng-class="{'has-error': addRecordForm.$submitted && addRecordForm['replacement_' + ($index)].$invalid}">
<input name="target_{{$index}}"
class="form-control"
ng-model="item.replacement"
ng-class="recordModal.details.class"
ng-readonly="recordModal.details.readOnly"
required>
</input>
</td>
<td>
<button type="button" class="btn btn-sm btn-danger fa fa-times"
ng-disabled="disabledStates.indexOf(recordModal.action) > -1"
ng-click="deleteNaptr($index)">
</button>
</td>
</tr>
</table>
<button type="button" class="btn btn-success"
ng-disabled="disabledStates.indexOf(recordModal.action) > -1" ng-click="addNewNaptr()">
Add Row
</button>
</modal-element>
<modal-element ng-if="currentRecord.type == 'SSHFP'" label="Record Data"> <modal-element ng-if="currentRecord.type == 'SSHFP'" label="Record Data">
<table class="table table-condensed"> <table class="table table-condensed">
<tr> <tr>