From 8ed1edbecc02f19c708df8b2d08999ce7ef5e381 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 30 Sep 2020 22:37:58 +0200 Subject: [PATCH 1/6] Add SVCB record TYPE (RR type 64) --- minidns-core/src/main/java/org/minidns/record/Record.java | 1 + 1 file changed, 1 insertion(+) diff --git a/minidns-core/src/main/java/org/minidns/record/Record.java b/minidns-core/src/main/java/org/minidns/record/Record.java index 359238fe..6d7dbcef 100644 --- a/minidns-core/src/main/java/org/minidns/record/Record.java +++ b/minidns-core/src/main/java/org/minidns/record/Record.java @@ -99,6 +99,7 @@ public enum TYPE { CDNSKEY(60), OPENPGPKEY(61, OPENPGPKEY.class), CSYNC(62), + SVCB(64), SPF(99), UINFO(100), UID(101), From c7ec8fb9ad96489d5b732e938c0e66efb946952d Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 30 Sep 2020 23:56:13 +0200 Subject: [PATCH 2/6] Add SVCB data class --- .../main/java/org/minidns/record/Record.java | 5 +- .../main/java/org/minidns/record/SVCB.java | 102 ++++++++++++++++++ .../java/org/minidns/record/RecordsTest.java | 19 ++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 minidns-core/src/main/java/org/minidns/record/SVCB.java diff --git a/minidns-core/src/main/java/org/minidns/record/Record.java b/minidns-core/src/main/java/org/minidns/record/Record.java index 6d7dbcef..013616bb 100644 --- a/minidns-core/src/main/java/org/minidns/record/Record.java +++ b/minidns-core/src/main/java/org/minidns/record/Record.java @@ -99,7 +99,7 @@ public enum TYPE { CDNSKEY(60), OPENPGPKEY(61, OPENPGPKEY.class), CSYNC(62), - SVCB(64), + SVCB(64, SVCB.class), SPF(99), UINFO(100), UID(101), @@ -400,6 +400,9 @@ public static Record parse(DataInputStream dis, byte[] data) throws IOExce case DLV: payloadData = DLV.parse(dis, payloadLength); break; + case SVCB: + payloadData = SVCB.parse(dis, payloadLength, data); + break; case UNKNOWN: default: payloadData = UNKNOWN.parse(dis, payloadLength, type); diff --git a/minidns-core/src/main/java/org/minidns/record/SVCB.java b/minidns-core/src/main/java/org/minidns/record/SVCB.java new file mode 100644 index 00000000..f4f427c6 --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/record/SVCB.java @@ -0,0 +1,102 @@ +package org.minidns.record; + +import org.minidns.dnsname.DnsName; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * SVCB Record Type (Service binding) + * + * https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01 + */ +class SVCB extends RRWithTarget { + + /** + * The priority indicates the SvcRecordType. + * https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.4 + */ + public final int priority; + + /** + * SvcFieldValue + * A set of key=value pairs. + * https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1 + */ + public final Map values; + + // The first group is the key. They key can only be a-z, 0-9 or "-" + // The second group is the value. It can be a lot of things (see https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1.1) + // except for DQUOTE (hence it can be excluded from the regex-group) + private static final Pattern valuesPattern = Pattern.compile("([a-z0-9\\-]+)=\"([^\"]*)\""); + + /** + * @param priority SvcRecordType + * @param target SvcDomainName + * @param values SvcFieldValue + */ + public SVCB(int priority, DnsName target, Map values) { + super(target); + this.priority = priority; + this.values = values; + } + + public static SVCB parse(DataInputStream dis, int length, byte[] data) + throws IOException { + int priority = dis.readUnsignedShort(); + DnsName target = DnsName.parse(dis, data); + + byte[] valuesBlob = new byte[length - 2 - target.getRawBytes().length]; + dis.readFully(valuesBlob); + return new SVCB(priority, target, parseValuesBlob(valuesBlob)); + } + + /** + * Parses pairs according to format from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1.1 + */ + private static Map parseValuesBlob(byte[] blob) { + Map values = new LinkedHashMap<>(); + String blobAsString = new String(blob, StandardCharsets.UTF_8); + Matcher matcher = valuesPattern.matcher(blobAsString); + while(matcher.find()) { + values.put(matcher.group(1), matcher.group(2)); + } + return values; + } + + @Override + public Record.TYPE getType() { + return Record.TYPE.SVCB; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeShort(priority); + super.serialize(dos); + dos.write(createValuesString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String toString() { + return priority + " " + target + createValuesString(); + } + + private String createValuesString() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : values.entrySet()) { + builder.append(" "); + builder.append(entry.getKey()); + builder.append("="); + builder.append("\""); + builder.append(entry.getValue()); + builder.append("\""); + } + return builder.toString(); + } +} diff --git a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java index 1cb3b7e7..d4f4e3aa 100644 --- a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java +++ b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java @@ -12,6 +12,7 @@ import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.dnsname.DnsName; import org.minidns.record.NSEC3.HashAlgorithm; import org.minidns.record.Record.TYPE; import org.junit.jupiter.api.Test; @@ -21,7 +22,10 @@ import java.io.IOException; import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static org.minidns.Assert.assertCsEquals; import static org.minidns.Assert.assertArrayContentEquals; @@ -48,6 +52,21 @@ public void testARecord() throws Exception { assertArrayEquals(new byte[] {127, 0, 0, 1}, a.getIp()); } + @Test + public void testSVCBRecord() throws Exception { + Map values = new LinkedHashMap<>(); + values.put("just", "testing"); + values.put("lookma", "nopläintêxt"); + values.put("even", "numbers like 1 and very long text with spaces in it."); + SVCB svcb = new SVCB(1, DnsName.from("example.com"), values); + + String expectedString = "1 example.com just=\"testing\" lookma=\"nopläintêxt\" even=\"numbers like 1 and very long text with spaces in it.\""; + assertEquals(expectedString, svcb.toString()); + byte[] svcbb = svcb.toByteArray(); + svcb = SVCB.parse(new DataInputStream(new ByteArrayInputStream(svcbb)), svcb.length(), svcbb); + assertEquals(expectedString, svcb.toString()); + } + @Test public void testARecordInvalidIp() throws Exception { assertThrows(IllegalArgumentException.class, () -> From c3f72106c2059ac6d9cf7d93d5635ed5946b4004 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 30 Sep 2020 23:57:18 +0200 Subject: [PATCH 3/6] Add test for type --- minidns-core/src/test/java/org/minidns/record/RecordsTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java index d4f4e3aa..2594b069 100644 --- a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java +++ b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java @@ -60,6 +60,8 @@ public void testSVCBRecord() throws Exception { values.put("even", "numbers like 1 and very long text with spaces in it."); SVCB svcb = new SVCB(1, DnsName.from("example.com"), values); + assertEquals(TYPE.SVCB, svcb.getType()); + String expectedString = "1 example.com just=\"testing\" lookma=\"nopläintêxt\" even=\"numbers like 1 and very long text with spaces in it.\""; assertEquals(expectedString, svcb.toString()); byte[] svcbb = svcb.toByteArray(); From 28def2cb8d01a2dc97b4f307d51d8de13db2efc0 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 1 Oct 2020 12:43:26 +0200 Subject: [PATCH 4/6] Use @see for links --- .../src/main/java/org/minidns/record/SVCB.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/minidns-core/src/main/java/org/minidns/record/SVCB.java b/minidns-core/src/main/java/org/minidns/record/SVCB.java index f4f427c6..e64c8a04 100644 --- a/minidns-core/src/main/java/org/minidns/record/SVCB.java +++ b/minidns-core/src/main/java/org/minidns/record/SVCB.java @@ -5,7 +5,6 @@ import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; @@ -14,26 +13,30 @@ /** * SVCB Record Type (Service binding) * - * https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01 + * @see draft-ietf-dnsop-svcb-https-01: Service binding and parameter specification via the DNS (DNS SVCB and HTTPS RRs) */ class SVCB extends RRWithTarget { /** - * The priority indicates the SvcRecordType. - * https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.4 + * The priority indicates the SvcPriority. + * A SvcPriority of 0 puts this RR in AliasMode (otherwise ServiceMode). + * + * @see Possible parameter IDs */ public final Map values; // The first group is the key. They key can only be a-z, 0-9 or "-" // The second group is the value. It can be a lot of things (see https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1.1) - // except for DQUOTE (hence it can be excluded from the regex-group) + // except for DQUOTE (hence it can be excluded from the regex-group) private static final Pattern valuesPattern = Pattern.compile("([a-z0-9\\-]+)=\"([^\"]*)\""); /** @@ -57,9 +60,6 @@ public static SVCB parse(DataInputStream dis, int length, byte[] data) return new SVCB(priority, target, parseValuesBlob(valuesBlob)); } - /** - * Parses pairs according to format from https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1.1 - */ private static Map parseValuesBlob(byte[] blob) { Map values = new LinkedHashMap<>(); String blobAsString = new String(blob, StandardCharsets.UTF_8); From c10a3f5a43de8042f77f2267881f453a03f7f747 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 1 Oct 2020 16:33:38 +0200 Subject: [PATCH 5/6] Add correct wireformat, add possible ServiceKey values, save params as unmodifiable map --- .../org/minidns/constants/SVCBConstants.java | 66 ++++++++++++ .../main/java/org/minidns/record/SVCB.java | 102 ++++++++++++------ .../java/org/minidns/record/RecordsTest.java | 27 ++++- 3 files changed, 157 insertions(+), 38 deletions(-) create mode 100644 minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java diff --git a/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java b/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java new file mode 100644 index 00000000..df8348ce --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java @@ -0,0 +1,66 @@ +package org.minidns.constants; + +public class SVCBConstants { + public interface ServiceKeySpecification { + int getNumber(); + String getTextualRepresentation(); + } + + public enum ServiceKey implements ServiceKeySpecification { + MANDATORY(0, "mandatory"), + ALPN(1, "alpn"), + NO_DEFAULT_ALPN(2, "no-default-alpn"), + PORT(3, "port"), + IPV4HINT(4, "ipv4hint"), + ECHOCONFIG(5, "echoconfig"), + IPV6HINT(6, "ipv6hint"), + INVALID_KEY(65535, "key65535"); + + private final int number; + private final String name; + ServiceKey(int number, String name) { + this.number = number; + this.name = name; + } + + @Override + public int getNumber() { + return number; + } + + @Override + public String getTextualRepresentation() { + return name; + } + + public static ServiceKeySpecification findFrom(int number) { + for (ServiceKey value : values()) { + if(value.number == number) return value; + } + return new UnrecognizedServiceKey(number); + } + } + + public static final class UnrecognizedServiceKey implements ServiceKeySpecification { + private final int number; + + public UnrecognizedServiceKey(int number) { + this.number = number; + } + + @Override + public int getNumber() { + return number; + } + + @Override + public String getTextualRepresentation() { + return String.valueOf(number); + } + + @Override + public String toString() { + return "key" + number; + } + } +} diff --git a/minidns-core/src/main/java/org/minidns/record/SVCB.java b/minidns-core/src/main/java/org/minidns/record/SVCB.java index e64c8a04..5775cb2b 100644 --- a/minidns-core/src/main/java/org/minidns/record/SVCB.java +++ b/minidns-core/src/main/java/org/minidns/record/SVCB.java @@ -1,14 +1,16 @@ package org.minidns.record; +import org.minidns.constants.SVCBConstants; import org.minidns.dnsname.DnsName; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.TreeMap; /** * SVCB Record Type (Service binding) @@ -26,60 +28,94 @@ class SVCB extends RRWithTarget { public final int priority; /** - * SvcFieldValue - * A set of key=value pairs. + * A set of key=value pairs (SvcFieldValue). * The key is an ID for the parameter. * + * This is a sorted map to follow specification. + * * @see Possible parameter IDs */ - public final Map values; - - // The first group is the key. They key can only be a-z, 0-9 or "-" - // The second group is the value. It can be a lot of things (see https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-01#section-2.1.1) - // except for DQUOTE (hence it can be excluded from the regex-group) - private static final Pattern valuesPattern = Pattern.compile("([a-z0-9\\-]+)=\"([^\"]*)\""); + public final Map params; /** - * @param priority SvcRecordType - * @param target SvcDomainName - * @param values SvcFieldValue + * @param priority SvcPriority + * @param target TargetName + * @param params SvcParams */ - public SVCB(int priority, DnsName target, Map values) { + public SVCB(int priority, DnsName target, Map params) { super(target); this.priority = priority; - this.values = values; + TreeMap sorted = new TreeMap<>(new Comparator() { + @Override + public int compare(SVCBConstants.ServiceKeySpecification first, SVCBConstants.ServiceKeySpecification other) { + return first.getNumber() - other.getNumber(); //Ascending order + } + }); + sorted.putAll(params); + this.params = Collections.unmodifiableSortedMap(sorted); } + /** + * Parses the wireformat data according to the spec. + * + * @see RDATA wire format specification + */ public static SVCB parse(DataInputStream dis, int length, byte[] data) throws IOException { int priority = dis.readUnsignedShort(); DnsName target = DnsName.parse(dis, data); + Map params; - byte[] valuesBlob = new byte[length - 2 - target.getRawBytes().length]; - dis.readFully(valuesBlob); - return new SVCB(priority, target, parseValuesBlob(valuesBlob)); - } - - private static Map parseValuesBlob(byte[] blob) { - Map values = new LinkedHashMap<>(); - String blobAsString = new String(blob, StandardCharsets.UTF_8); - Matcher matcher = valuesPattern.matcher(blobAsString); - while(matcher.find()) { - values.put(matcher.group(1), matcher.group(2)); + int paramBlobSize = length - 2 - target.getRawBytes().length; + if(paramBlobSize == 0) { + params = Collections.emptyMap(); + } else { + params = parseParamsBlob(dis, paramBlobSize); } - return values; + + return new SVCB(priority, target, params); } - @Override - public Record.TYPE getType() { - return Record.TYPE.SVCB; + private static Map parseParamsBlob(DataInputStream dis, int paramBlobSize) throws IOException { + int remainingBytes = paramBlobSize; + int lastKey = Integer.MIN_VALUE; + Map params = new LinkedHashMap<>(); + + while(remainingBytes > 0) { + int key = dis.readUnsignedShort(); + String value = null; + if(key < lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must be in ascending order"); + else if(key == lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must not be duplicate"); + lastKey = key; + + int valueLength = dis.readUnsignedShort(); + if(valueLength != 0) { + byte[] valueBlob = new byte[valueLength]; + dis.readFully(valueBlob); + value = new String(valueBlob, StandardCharsets.UTF_8); + } + + params.put(SVCBConstants.ServiceKey.findFrom(key), value); + remainingBytes = remainingBytes - 4 - valueLength; + } + return Collections.unmodifiableMap(params); } @Override public void serialize(DataOutputStream dos) throws IOException { dos.writeShort(priority); super.serialize(dos); - dos.write(createValuesString().getBytes(StandardCharsets.UTF_8)); + for (Map.Entry entry : params.entrySet()) { + dos.writeShort(entry.getKey().getNumber()); + byte[] paramValueBlob = entry.getValue().getBytes(StandardCharsets.UTF_8); + dos.writeShort(paramValueBlob.length); + dos.write(paramValueBlob); + } + } + + @Override + public Record.TYPE getType() { + return Record.TYPE.SVCB; } @Override @@ -89,9 +125,9 @@ public String toString() { private String createValuesString() { StringBuilder builder = new StringBuilder(); - for (Map.Entry entry : values.entrySet()) { + for (Map.Entry entry : params.entrySet()) { builder.append(" "); - builder.append(entry.getKey()); + builder.append(entry.getKey().getTextualRepresentation()); builder.append("="); builder.append("\""); builder.append(entry.getValue()); diff --git a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java index 2594b069..4110f88e 100644 --- a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java +++ b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java @@ -12,6 +12,7 @@ import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.constants.SVCBConstants; import org.minidns.dnsname.DnsName; import org.minidns.record.NSEC3.HashAlgorithm; import org.minidns.record.Record.TYPE; @@ -54,15 +55,31 @@ public void testARecord() throws Exception { @Test public void testSVCBRecord() throws Exception { - Map values = new LinkedHashMap<>(); - values.put("just", "testing"); - values.put("lookma", "nopläintêxt"); - values.put("even", "numbers like 1 and very long text with spaces in it."); + Map values = new HashMap<>(); + values.put(SVCBConstants.ServiceKey.IPV6HINT, "testing"); //Number = 6 + values.put(SVCBConstants.ServiceKey.ALPN, "nopläintêxt"); // Number = 1 + values.put(SVCBConstants.ServiceKey.PORT, "numbers like 1 and very, very long text with spaces in it."); // Number = 3 + values.put(new SVCBConstants.UnrecognizedServiceKey(65281), "unknown"); // 65281 is in the private use block. SVCB svcb = new SVCB(1, DnsName.from("example.com"), values); assertEquals(TYPE.SVCB, svcb.getType()); - String expectedString = "1 example.com just=\"testing\" lookma=\"nopläintêxt\" even=\"numbers like 1 and very long text with spaces in it.\""; + // The keys should be ordered ascending + String expectedString = "1 example.com alpn=\"nopläintêxt\" port=\"numbers like 1 and very, very long text with spaces in it.\" ipv6hint=\"testing\" 65281=\"unknown\""; + assertEquals(expectedString, svcb.toString()); + byte[] svcbb = svcb.toByteArray(); + svcb = SVCB.parse(new DataInputStream(new ByteArrayInputStream(svcbb)), svcb.length(), svcbb); + assertEquals(expectedString, svcb.toString()); + } + + @Test + public void testSVCBRecord_NoParams() throws Exception { + SVCB svcb = new SVCB(0, DnsName.from("example.com"), Collections.emptyMap()); + + assertEquals(TYPE.SVCB, svcb.getType()); + + // The keys should be ordered ascending + String expectedString = "1 example.com"; assertEquals(expectedString, svcb.toString()); byte[] svcbb = svcb.toByteArray(); svcb = SVCB.parse(new DataInputStream(new ByteArrayInputStream(svcbb)), svcb.length(), svcbb); From 5c4b8c475ed869904f4c65ce4e8fc04fd8a3b00e Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Thu, 1 Oct 2020 22:33:35 +0200 Subject: [PATCH 6/6] Current (broken) state --- .../org/minidns/constants/SVCBConstants.java | 73 ++++--------------- .../svcbservicekeys/ALPNServiceKey.java | 50 +++++++++++++ .../ServiceKeySpecification.java | 31 ++++++++ .../UnrecognizedServiceKey.java | 24 ++++++ .../main/java/org/minidns/record/Record.java | 2 +- .../main/java/org/minidns/record/SVCB.java | 68 ++++++++--------- .../java/org/minidns/util/RRTextUtil.java | 13 ++++ .../java/org/minidns/record/RecordsTest.java | 33 +++------ 8 files changed, 174 insertions(+), 120 deletions(-) create mode 100644 minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ALPNServiceKey.java create mode 100644 minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ServiceKeySpecification.java create mode 100644 minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/UnrecognizedServiceKey.java create mode 100644 minidns-core/src/main/java/org/minidns/util/RRTextUtil.java diff --git a/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java b/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java index df8348ce..6f387cef 100644 --- a/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java +++ b/minidns-core/src/main/java/org/minidns/constants/SVCBConstants.java @@ -1,66 +1,23 @@ package org.minidns.constants; -public class SVCBConstants { - public interface ServiceKeySpecification { - int getNumber(); - String getTextualRepresentation(); - } +import org.minidns.constants.svcbservicekeys.ALPNServiceKey; +import org.minidns.constants.svcbservicekeys.ServiceKeySpecification; +import org.minidns.constants.svcbservicekeys.UnrecognizedServiceKey; - public enum ServiceKey implements ServiceKeySpecification { - MANDATORY(0, "mandatory"), - ALPN(1, "alpn"), - NO_DEFAULT_ALPN(2, "no-default-alpn"), - PORT(3, "port"), - IPV4HINT(4, "ipv4hint"), - ECHOCONFIG(5, "echoconfig"), - IPV6HINT(6, "ipv6hint"), - INVALID_KEY(65535, "key65535"); - private final int number; - private final String name; - ServiceKey(int number, String name) { - this.number = number; - this.name = name; - } - - @Override - public int getNumber() { - return number; - } - - @Override - public String getTextualRepresentation() { - return name; - } - - public static ServiceKeySpecification findFrom(int number) { - for (ServiceKey value : values()) { - if(value.number == number) return value; - } - return new UnrecognizedServiceKey(number); +public class SVCBConstants { + public static ServiceKeySpecification findServiceKeyByNumber(int number, byte[] blob) { + switch (number) { + case 1: return new ALPNServiceKey(blob); + default: return new UnrecognizedServiceKey(blob, number); } } - public static final class UnrecognizedServiceKey implements ServiceKeySpecification { - private final int number; - - public UnrecognizedServiceKey(int number) { - this.number = number; - } - - @Override - public int getNumber() { - return number; - } - - @Override - public String getTextualRepresentation() { - return String.valueOf(number); - } - - @Override - public String toString() { - return "key" + number; - } - } + // ALPN(1, "alpn"), + // NO_DEFAULT_ALPN(2, "no-default-alpn"), + // PORT(3, "port"), + // IPV4HINT(4, "ipv4hint"), + // ECHOCONFIG(5, "echoconfig"), + // IPV6HINT(6, "ipv6hint"), + // INVALID_KEY(65535, "key65535"); } diff --git a/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ALPNServiceKey.java b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ALPNServiceKey.java new file mode 100644 index 00000000..75de724a --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ALPNServiceKey.java @@ -0,0 +1,50 @@ +package org.minidns.constants.svcbservicekeys; + +import org.minidns.util.RRTextUtil; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ALPNServiceKey extends ServiceKeySpecification> { + private List value; + + public ALPNServiceKey(byte[] blob) { + super(blob, 1); + } + + @Override + public List value() throws IOException { + if(value == null) { + List values = new ArrayList<>(); + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob)); + while(dis.available() > 0) { + byte[] blob = new byte[dis.readUnsignedShort()]; + dis.readFully(blob); + values.add(RRTextUtil.getTextFrom(blob)); + } + value = Collections.unmodifiableList(values); + } + return value; + } + + @Override + public String getTextualRepresentation() { + return "alpn"; + } + + @Override + public String valueAsString() throws IOException { + StringBuilder sb = new StringBuilder(); + for (String s : value()) { + if(sb.length() > 0) { + sb.append(","); + } + sb.append(s.replaceAll(",", "\\\\,")); + } + return sb.toString(); + } +} diff --git a/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ServiceKeySpecification.java b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ServiceKeySpecification.java new file mode 100644 index 00000000..7a34dfbc --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/ServiceKeySpecification.java @@ -0,0 +1,31 @@ +package org.minidns.constants.svcbservicekeys; + +import java.io.IOException; + +public abstract class ServiceKeySpecification implements Comparable> { + public final byte[] blob; + public final int number; + + public ServiceKeySpecification(byte[] blob, int number) { + this.blob = blob; + this.number = number; + } + + public final int getNumber() { + return number; + } + + abstract public ValueType value() throws IOException; + abstract public String getTextualRepresentation(); + abstract public String valueAsString() throws IOException; + + @Override + public int compareTo(ServiceKeySpecification other) { + return getNumber() - other.getNumber(); + } + + @Override + public String toString() { + return getTextualRepresentation(); + } +} \ No newline at end of file diff --git a/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/UnrecognizedServiceKey.java b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/UnrecognizedServiceKey.java new file mode 100644 index 00000000..2bcb8711 --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/constants/svcbservicekeys/UnrecognizedServiceKey.java @@ -0,0 +1,24 @@ +package org.minidns.constants.svcbservicekeys; + +import java.util.Arrays; + +public class UnrecognizedServiceKey extends ServiceKeySpecification{ + public UnrecognizedServiceKey(byte[] blob, int number) { + super(blob, number); + } + + @Override + public byte[] value() { + return blob; + } + + @Override + public String getTextualRepresentation() { + return "key" + number; + } + + @Override + public String valueAsString() { + return Arrays.toString(blob); + } +} diff --git a/minidns-core/src/main/java/org/minidns/record/Record.java b/minidns-core/src/main/java/org/minidns/record/Record.java index 013616bb..37fd4f71 100644 --- a/minidns-core/src/main/java/org/minidns/record/Record.java +++ b/minidns-core/src/main/java/org/minidns/record/Record.java @@ -99,7 +99,7 @@ public enum TYPE { CDNSKEY(60), OPENPGPKEY(61, OPENPGPKEY.class), CSYNC(62), - SVCB(64, SVCB.class), + SVCB(65, SVCB.class), SPF(99), UINFO(100), UID(101), diff --git a/minidns-core/src/main/java/org/minidns/record/SVCB.java b/minidns-core/src/main/java/org/minidns/record/SVCB.java index 5775cb2b..61f88aee 100644 --- a/minidns-core/src/main/java/org/minidns/record/SVCB.java +++ b/minidns-core/src/main/java/org/minidns/record/SVCB.java @@ -1,16 +1,15 @@ package org.minidns.record; import org.minidns.constants.SVCBConstants; +import org.minidns.constants.svcbservicekeys.ServiceKeySpecification; import org.minidns.dnsname.DnsName; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TreeMap; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; /** * SVCB Record Type (Service binding) @@ -35,24 +34,18 @@ class SVCB extends RRWithTarget { * * @see Possible parameter IDs */ - public final Map params; + public final Set> params; /** * @param priority SvcPriority * @param target TargetName * @param params SvcParams */ - public SVCB(int priority, DnsName target, Map params) { + public SVCB(int priority, DnsName target, Set> params) { super(target); this.priority = priority; - TreeMap sorted = new TreeMap<>(new Comparator() { - @Override - public int compare(SVCBConstants.ServiceKeySpecification first, SVCBConstants.ServiceKeySpecification other) { - return first.getNumber() - other.getNumber(); //Ascending order - } - }); - sorted.putAll(params); - this.params = Collections.unmodifiableSortedMap(sorted); + TreeSet> sorted = new TreeSet<>(params); + this.params = Collections.unmodifiableSortedSet(sorted); } /** @@ -64,52 +57,49 @@ public static SVCB parse(DataInputStream dis, int length, byte[] data) throws IOException { int priority = dis.readUnsignedShort(); DnsName target = DnsName.parse(dis, data); - Map params; + Set> params; int paramBlobSize = length - 2 - target.getRawBytes().length; if(paramBlobSize == 0) { - params = Collections.emptyMap(); + params = Collections.emptySet(); } else { - params = parseParamsBlob(dis, paramBlobSize); + params = parseParamsBlob(dis, length); } return new SVCB(priority, target, params); } - private static Map parseParamsBlob(DataInputStream dis, int paramBlobSize) throws IOException { + private static Set> parseParamsBlob(DataInputStream dis, int paramBlobSize) throws IOException { int remainingBytes = paramBlobSize; int lastKey = Integer.MIN_VALUE; - Map params = new LinkedHashMap<>(); + Set> params = new HashSet<>(); while(remainingBytes > 0) { int key = dis.readUnsignedShort(); - String value = null; - if(key < lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must be in ascending order"); - else if(key == lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must not be duplicate"); + if(key < lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must be in ascending order (" + key + "<" + lastKey + ")"); + else if(key == lastKey) throw new IllegalArgumentException("SVCB ServiceKeys must not be duplicate (" + key + "=" + lastKey + ")"); lastKey = key; int valueLength = dis.readUnsignedShort(); + byte[] valueBlob = new byte[valueLength]; if(valueLength != 0) { - byte[] valueBlob = new byte[valueLength]; dis.readFully(valueBlob); - value = new String(valueBlob, StandardCharsets.UTF_8); } - params.put(SVCBConstants.ServiceKey.findFrom(key), value); + ServiceKeySpecification detectedKey = SVCBConstants.findServiceKeyByNumber(key, valueBlob); + params.add(detectedKey); remainingBytes = remainingBytes - 4 - valueLength; } - return Collections.unmodifiableMap(params); + return params; } @Override public void serialize(DataOutputStream dos) throws IOException { dos.writeShort(priority); super.serialize(dos); - for (Map.Entry entry : params.entrySet()) { - dos.writeShort(entry.getKey().getNumber()); - byte[] paramValueBlob = entry.getValue().getBytes(StandardCharsets.UTF_8); - dos.writeShort(paramValueBlob.length); - dos.write(paramValueBlob); + for (ServiceKeySpecification param: params) { + dos.writeShort(param.blob.length); + dos.write(param.blob); } } @@ -120,17 +110,21 @@ public Record.TYPE getType() { @Override public String toString() { - return priority + " " + target + createValuesString(); + try { + return priority + " " + target + createValuesString(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - private String createValuesString() { + private String createValuesString() throws IOException { StringBuilder builder = new StringBuilder(); - for (Map.Entry entry : params.entrySet()) { + for (ServiceKeySpecification param : params) { builder.append(" "); - builder.append(entry.getKey().getTextualRepresentation()); + builder.append(param.getTextualRepresentation()); builder.append("="); builder.append("\""); - builder.append(entry.getValue()); + builder.append(param.valueAsString()); builder.append("\""); } return builder.toString(); diff --git a/minidns-core/src/main/java/org/minidns/util/RRTextUtil.java b/minidns-core/src/main/java/org/minidns/util/RRTextUtil.java new file mode 100644 index 00000000..df5caa56 --- /dev/null +++ b/minidns-core/src/main/java/org/minidns/util/RRTextUtil.java @@ -0,0 +1,13 @@ +package org.minidns.util; + +public class RRTextUtil { + + public static String getTextFrom(byte[] blob) { + StringBuilder sb = new StringBuilder(); + return sb.toString(); + } + + public static byte[] textToByteArray(String s) { + return new byte[0]; + } +} diff --git a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java index 4110f88e..dbbf459f 100644 --- a/minidns-core/src/test/java/org/minidns/record/RecordsTest.java +++ b/minidns-core/src/test/java/org/minidns/record/RecordsTest.java @@ -12,7 +12,8 @@ import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; -import org.minidns.constants.SVCBConstants; +import org.minidns.constants.svcbservicekeys.ServiceKeySpecification; +import org.minidns.constants.svcbservicekeys.UnrecognizedServiceKey; import org.minidns.dnsname.DnsName; import org.minidns.record.NSEC3.HashAlgorithm; import org.minidns.record.Record.TYPE; @@ -23,10 +24,9 @@ import java.io.IOException; import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import static org.minidns.Assert.assertCsEquals; import static org.minidns.Assert.assertArrayContentEquals; @@ -55,12 +55,11 @@ public void testARecord() throws Exception { @Test public void testSVCBRecord() throws Exception { - Map values = new HashMap<>(); - values.put(SVCBConstants.ServiceKey.IPV6HINT, "testing"); //Number = 6 - values.put(SVCBConstants.ServiceKey.ALPN, "nopläintêxt"); // Number = 1 - values.put(SVCBConstants.ServiceKey.PORT, "numbers like 1 and very, very long text with spaces in it."); // Number = 3 - values.put(new SVCBConstants.UnrecognizedServiceKey(65281), "unknown"); // 65281 is in the private use block. - SVCB svcb = new SVCB(1, DnsName.from("example.com"), values); + Set> params = new TreeSet<>(); + params.add(new UnrecognizedServiceKey(new byte[1], 6)); + params.add(new UnrecognizedServiceKey(new byte[1], 1)); + params.add(new UnrecognizedServiceKey(new byte[1], 3)); + SVCB svcb = new SVCB(1, DnsName.from("example.com"), params); assertEquals(TYPE.SVCB, svcb.getType()); @@ -72,20 +71,6 @@ public void testSVCBRecord() throws Exception { assertEquals(expectedString, svcb.toString()); } - @Test - public void testSVCBRecord_NoParams() throws Exception { - SVCB svcb = new SVCB(0, DnsName.from("example.com"), Collections.emptyMap()); - - assertEquals(TYPE.SVCB, svcb.getType()); - - // The keys should be ordered ascending - String expectedString = "1 example.com"; - assertEquals(expectedString, svcb.toString()); - byte[] svcbb = svcb.toByteArray(); - svcb = SVCB.parse(new DataInputStream(new ByteArrayInputStream(svcbb)), svcb.length(), svcbb); - assertEquals(expectedString, svcb.toString()); - } - @Test public void testARecordInvalidIp() throws Exception { assertThrows(IllegalArgumentException.class, () ->