diff --git a/src/main/generated/io/vertx/core/dns/DnsClientOptionsConverter.java b/src/main/generated/io/vertx/core/dns/DnsClientOptionsConverter.java index b9eb31a45f1..2dcacc41e3b 100644 --- a/src/main/generated/io/vertx/core/dns/DnsClientOptionsConverter.java +++ b/src/main/generated/io/vertx/core/dns/DnsClientOptionsConverter.java @@ -50,6 +50,11 @@ static void fromJson(Iterable> json, DnsClie obj.setRecursionDesired((Boolean)member.getValue()); } break; + case "ssl": + if (member.getValue() instanceof Boolean) { + obj.setSsl((Boolean)member.getValue()); + } + break; } } } @@ -69,5 +74,6 @@ static void toJson(DnsClientOptions obj, java.util.Map json) { json.put("port", obj.getPort()); json.put("queryTimeout", obj.getQueryTimeout()); json.put("recursionDesired", obj.isRecursionDesired()); + json.put("ssl", obj.isSsl()); } } diff --git a/src/main/generated/io/vertx/core/dns/dnsrecord/DohRecordConverter.java b/src/main/generated/io/vertx/core/dns/dnsrecord/DohRecordConverter.java new file mode 100644 index 00000000000..e334876d1e5 --- /dev/null +++ b/src/main/generated/io/vertx/core/dns/dnsrecord/DohRecordConverter.java @@ -0,0 +1,129 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.core.dns.dnsrecord.DohRecord}. + * NOTE: This class has been automatically generated from the {@link io.vertx.core.dns.dnsrecord.DohRecord} original class using Vert.x codegen. + */ +public class DohRecordConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, DohRecord obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "ad": + if (member.getValue() instanceof Boolean) { + obj.setAd((Boolean)member.getValue()); + } + break; + case "additional": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable)member.getValue()).forEach( item -> { + if (item instanceof JsonObject) + list.add(new io.vertx.core.dns.dnsrecord.DohResourceRecord((io.vertx.core.json.JsonObject)item)); + }); + obj.setAdditional(list); + } + break; + case "answer": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable)member.getValue()).forEach( item -> { + if (item instanceof JsonObject) + list.add(new io.vertx.core.dns.dnsrecord.DohResourceRecord((io.vertx.core.json.JsonObject)item)); + }); + obj.setAnswer(list); + } + break; + case "authority": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable)member.getValue()).forEach( item -> { + if (item instanceof JsonObject) + list.add(new io.vertx.core.dns.dnsrecord.DohResourceRecord((io.vertx.core.json.JsonObject)item)); + }); + obj.setAuthority(list); + } + break; + case "cd": + if (member.getValue() instanceof Boolean) { + obj.setCd((Boolean)member.getValue()); + } + break; + case "question": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable)member.getValue()).forEach( item -> { + if (item instanceof JsonObject) + list.add(new io.vertx.core.dns.dnsrecord.Question((io.vertx.core.json.JsonObject)item)); + }); + obj.setQuestion(list); + } + break; + case "ra": + if (member.getValue() instanceof Boolean) { + obj.setRa((Boolean)member.getValue()); + } + break; + case "rd": + if (member.getValue() instanceof Boolean) { + obj.setRd((Boolean)member.getValue()); + } + break; + case "status": + if (member.getValue() instanceof Number) { + obj.setStatus(((Number)member.getValue()).intValue()); + } + break; + case "tc": + if (member.getValue() instanceof Boolean) { + obj.setTc((Boolean)member.getValue()); + } + break; + } + } + } + + static void toJson(DohRecord obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(DohRecord obj, java.util.Map json) { + json.put("ad", obj.isAd()); + if (obj.getAdditional() != null) { + JsonArray array = new JsonArray(); + obj.getAdditional().forEach(item -> array.add(item.toJson())); + json.put("additional", array); + } + if (obj.getAnswer() != null) { + JsonArray array = new JsonArray(); + obj.getAnswer().forEach(item -> array.add(item.toJson())); + json.put("answer", array); + } + if (obj.getAuthority() != null) { + JsonArray array = new JsonArray(); + obj.getAuthority().forEach(item -> array.add(item.toJson())); + json.put("authority", array); + } + json.put("cd", obj.isCd()); + if (obj.getQuestion() != null) { + JsonArray array = new JsonArray(); + obj.getQuestion().forEach(item -> array.add(item.toJson())); + json.put("question", array); + } + json.put("ra", obj.isRa()); + json.put("rd", obj.isRd()); + json.put("status", obj.getStatus()); + json.put("tc", obj.isTc()); + } +} diff --git a/src/main/generated/io/vertx/core/dns/dnsrecord/DohResourceRecordConverter.java b/src/main/generated/io/vertx/core/dns/dnsrecord/DohResourceRecordConverter.java new file mode 100644 index 00000000000..773e73f3508 --- /dev/null +++ b/src/main/generated/io/vertx/core/dns/dnsrecord/DohResourceRecordConverter.java @@ -0,0 +1,61 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.core.dns.dnsrecord.DohResourceRecord}. + * NOTE: This class has been automatically generated from the {@link io.vertx.core.dns.dnsrecord.DohResourceRecord} original class using Vert.x codegen. + */ +public class DohResourceRecordConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, DohResourceRecord obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "data": + if (member.getValue() instanceof String) { + obj.setData((String)member.getValue()); + } + break; + case "name": + if (member.getValue() instanceof String) { + obj.setName((String)member.getValue()); + } + break; + case "ttl": + if (member.getValue() instanceof Number) { + obj.setTtl(((Number)member.getValue()).intValue()); + } + break; + case "type": + if (member.getValue() instanceof Number) { + obj.setType(((Number)member.getValue()).intValue()); + } + break; + } + } + } + + static void toJson(DohResourceRecord obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(DohResourceRecord obj, java.util.Map json) { + if (obj.getData() != null) { + json.put("data", obj.getData()); + } + if (obj.getName() != null) { + json.put("name", obj.getName()); + } + json.put("ttl", obj.getTtl()); + json.put("type", obj.getType()); + } +} diff --git a/src/main/generated/io/vertx/core/dns/dnsrecord/QuestionConverter.java b/src/main/generated/io/vertx/core/dns/dnsrecord/QuestionConverter.java new file mode 100644 index 00000000000..9e9439f0dee --- /dev/null +++ b/src/main/generated/io/vertx/core/dns/dnsrecord/QuestionConverter.java @@ -0,0 +1,47 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.impl.JsonUtil; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +/** + * Converter and mapper for {@link io.vertx.core.dns.dnsrecord.Question}. + * NOTE: This class has been automatically generated from the {@link io.vertx.core.dns.dnsrecord.Question} original class using Vert.x codegen. + */ +public class QuestionConverter { + + + private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; + private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; + + static void fromJson(Iterable> json, Question obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "name": + if (member.getValue() instanceof String) { + obj.setName((String)member.getValue()); + } + break; + case "type": + if (member.getValue() instanceof Number) { + obj.setType(((Number)member.getValue()).intValue()); + } + break; + } + } + } + + static void toJson(Question obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(Question obj, java.util.Map json) { + if (obj.getName() != null) { + json.put("name", obj.getName()); + } + json.put("type", obj.getType()); + } +} diff --git a/src/main/java/examples/DNSExamples.java b/src/main/java/examples/DNSExamples.java index 570cd0e6bd7..2b34da2f617 100644 --- a/src/main/java/examples/DNSExamples.java +++ b/src/main/java/examples/DNSExamples.java @@ -40,7 +40,8 @@ public void example1__(Vertx vertx) { DnsClient client1 = vertx.createDnsClient(); // Just the same but with a different query timeout - DnsClient client2 = vertx.createDnsClient(new DnsClientOptions().setQueryTimeout(10000)); + DnsClient client2 = + vertx.createDnsClient(new DnsClientOptions().setQueryTimeout(10000)); } public void example2(Vertx vertx) { @@ -49,9 +50,9 @@ public void example2(Vertx vertx) { .lookup("vertx.io") .onComplete(ar -> { if (ar.succeeded()) { - System.out.println(ar.result()); + System.out.printf("Dns lookup result: %s\n", ar.result()); } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns lookup entry" + ar.cause()); } }); } @@ -62,9 +63,9 @@ public void example3(Vertx vertx) { .lookup4("vertx.io") .onComplete(ar -> { if (ar.succeeded()) { - System.out.println(ar.result()); + System.out.printf("Dns lookup4 result: %s\n", ar.result()); } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns lookup4 entry" + ar.cause()); } }); } @@ -75,9 +76,9 @@ public void example4(Vertx vertx) { .lookup6("vertx.io") .onComplete(ar -> { if (ar.succeeded()) { - System.out.println(ar.result()); + System.out.printf("Dns lookup6 result: %s\n", ar.result()); } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns lookup6 entry" + ar.cause()); } }); } @@ -89,11 +90,11 @@ public void example5(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (String record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveA (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveA entry" + ar.cause()); } }); } @@ -105,11 +106,11 @@ public void example6(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (String record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveAAAA (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveAAAA entry" + ar.cause()); } }); } @@ -121,11 +122,11 @@ public void example7(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (String record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveCNAME (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveCNAME entry" + ar.cause()); } }); } @@ -137,11 +138,11 @@ public void example8(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (MxRecord record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveMX (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveMX entry" + ar.cause()); } }); } @@ -158,11 +159,11 @@ public void example10(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (String record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveTXT (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveTXT entry" + ar.cause()); } }); } @@ -174,11 +175,11 @@ public void example11(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (String record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveNS (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveNS entry" + ar.cause()); } }); } @@ -190,11 +191,11 @@ public void example12(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { List records = ar.result(); - for (SrvRecord record : records) { - System.out.println(record); + for (int i = 0; i < records.size(); i++) { + System.out.printf("Dns resolveSRV (%s): %s\n", i, records.get(i)); } } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolveSRV entry" + ar.cause()); } }); } @@ -220,9 +221,9 @@ public void example14(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { String record = ar.result(); - System.out.println(record); + System.out.printf("Dns resolvePTR result: %s\n", record); } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns resolvePTR entry" + ar.cause()); } }); } @@ -234,10 +235,224 @@ public void example15(Vertx vertx) { .onComplete(ar -> { if (ar.succeeded()) { String record = ar.result(); - System.out.println(record); + System.out.printf("Dns reverseLookup result: %s\n", record); } else { - System.out.println("Failed to resolve entry" + ar.cause()); + System.out.println("Failed to dns reverseLookup entry" + ar.cause()); } }); } + + public void example16(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .lookup("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + System.out.printf("DoH lookup result: %s\n", ar.result()); + } else { + System.out.println("Failed to DoH lookup entry" + ar.cause()); + } + }); + } + + public void example17(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .lookup4("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + System.out.printf("DoH lookup4 result: %s\n", ar.result()); + } else { + System.out.println("Failed to DoH lookup4 entry" + ar.cause()); + } + }); + } + + public void example18(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .lookup6("google.com") + .onComplete(ar -> { + if (ar.succeeded()) { + System.out.printf("DoH lookup6 result: %s\n", ar.result()); + } else { + System.out.println("Failed to DoH lookup6 entry" + ar.cause()); + } + }); + } + public void example19(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveA("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveA (%s): %s\n", i, records.get(i));} + } else { + System.out.println("Failed to DoH resolveA entry" + ar.cause()); + } + }); + } + + public void example20(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveAAAA("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveAAAA (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveAAAA entry" + ar.cause()); + } + }); + } + + public void example21(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveCNAME("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveCNAME (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveCNAME entry" + ar.cause()); + } + }); + } + + public void example22(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveMX("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveMX (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveMX entry" + ar.cause()); + } + }); + } + + public void example23(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveTXT("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveTXT (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveTXT entry" + ar.cause()); + } + }); + } + + public void example24(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveNS("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveNS (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveNS entry" + ar.cause()); + } + }); + } + + public void example25(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolveSRV("vertx.io") + .onComplete(ar -> { + if (ar.succeeded()) { + List records = ar.result(); + for (int i = 0; i < records.size(); i++) { + System.out.printf("DoH resolveSRV (%s): %s\n", i, records.get(i)); + } + } else { + System.out.println("Failed to DoH resolveSRV entry" + ar.cause()); + } + }); + } + + public void example26(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .resolvePTR("1.0.0.10.in-addr.arpa") + .onComplete(ar -> { + if (ar.succeeded()) { + String record = ar.result(); + System.out.printf("DoH resolvePTR result: %s\n", record); + } else { + System.out.println("Failed to DoH resolvePTR entry" + ar.cause()); + } + }); + } + + public void example27(Vertx vertx) { + DnsClient client = vertx.createDohClient(); + client + .reverseLookup("10.0.0.1") + .onComplete(ar -> { + if (ar.succeeded()) { + String record = ar.result(); + System.out.printf("DoH reverseLookup result: %s\n", record); + } else { + System.out.println("Failed to DoH reverseLookup entry" + ar.cause()); + } + }); + } + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + DNSExamples dnsExamples = new DNSExamples(); + + dnsExamples.example1(vertx); + dnsExamples.example1_(vertx); + dnsExamples.example1__(vertx); + dnsExamples.example2(vertx); + dnsExamples.example3(vertx); + dnsExamples.example4(vertx); + dnsExamples.example5(vertx); + dnsExamples.example6(vertx); + dnsExamples.example7(vertx); + dnsExamples.example8(vertx); + dnsExamples.example10(vertx); + dnsExamples.example11(vertx); + dnsExamples.example12(vertx); + dnsExamples.example14(vertx); + dnsExamples.example15(vertx); + + + dnsExamples.example16(vertx); + dnsExamples.example17(vertx); + dnsExamples.example18(vertx); + dnsExamples.example19(vertx); + dnsExamples.example20(vertx); + dnsExamples.example21(vertx); + dnsExamples.example22(vertx); + dnsExamples.example23(vertx); + dnsExamples.example24(vertx); + dnsExamples.example25(vertx); + dnsExamples.example26(vertx); + dnsExamples.example27(vertx); +// + } } diff --git a/src/main/java/io/vertx/core/Vertx.java b/src/main/java/io/vertx/core/Vertx.java index f8d3120bdfd..2a8df10bd78 100644 --- a/src/main/java/io/vertx/core/Vertx.java +++ b/src/main/java/io/vertx/core/Vertx.java @@ -356,6 +356,24 @@ default DatagramSocket createDatagramSocket() { */ DnsClient createDnsClient(); + /** + * Create a DoH client to connect to a DoH server at the specified host, with the default query timeout (5 seconds) + *

+ * + * @param host the host + * @return the DNS client + */ + DnsClient createDohClient(String host); + + /** + * Create a DoH client to connect to the DoH server configured by {@link VertxOptions#getAddressResolverOptions()} + *

+ * DNS client takes the first configured resolver address provided by {@link DnsResolverProvider#nameServerAddresses()}} + * + * @return the DNS client + */ + DnsClient createDohClient(); + /** * Create a DNS client to connect to a DNS server * diff --git a/src/main/java/io/vertx/core/dns/DnsClientOptions.java b/src/main/java/io/vertx/core/dns/DnsClientOptions.java index 45e2b3b6e0d..e2b20b3e39d 100644 --- a/src/main/java/io/vertx/core/dns/DnsClientOptions.java +++ b/src/main/java/io/vertx/core/dns/DnsClientOptions.java @@ -36,6 +36,11 @@ public class DnsClientOptions { */ public static final String DEFAULT_HOST = null; + /** + * The default value for the SSL = {@code false} (configured by {@link VertxOptions#getAddressResolverOptions()}) + */ + public static final boolean DEFAULT_SSL = false; + /** * The default value for the query timeout in millis = {@code 5000} */ @@ -58,6 +63,7 @@ public class DnsClientOptions { private int port = DEFAULT_PORT; private String host = DEFAULT_HOST; + private boolean ssl = DEFAULT_SSL; private long queryTimeout = DEFAULT_QUERY_TIMEOUT; private boolean logActivity = DEFAULT_LOG_ENABLED; private ByteBufFormat activityLogFormat = DEFAULT_LOG_ACTIVITY_FORMAT; @@ -77,6 +83,7 @@ public DnsClientOptions(DnsClientOptions other) { logActivity = other.logActivity; activityLogFormat = other.activityLogFormat; recursionDesired = other.recursionDesired; + ssl = other.ssl; } /** @@ -197,6 +204,25 @@ public DnsClientOptions setRecursionDesired(boolean recursionDesired) { return this; } + /** + * Get the ssl to be used by this client in requests. + * + * @return the ssl + */ + public boolean isSsl() { + return ssl; + } + + /** + * Set the ssl to be used by this client in requests. + * + * @return a reference to this, so the API can be used fluently + */ + public DnsClientOptions setSsl(boolean ssl) { + this.ssl = ssl; + return this; + } + public JsonObject toJson() { JsonObject json = new JsonObject(); DnsClientOptionsConverter.toJson(this, json); diff --git a/src/main/java/io/vertx/core/dns/dnsrecord/DohRecord.java b/src/main/java/io/vertx/core/dns/dnsrecord/DohRecord.java new file mode 100644 index 00000000000..dbffef78839 --- /dev/null +++ b/src/main/java/io/vertx/core/dns/dnsrecord/DohRecord.java @@ -0,0 +1,131 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +import java.util.List; + +/** + * @author Iman Zolfaghari + */ + +@DataObject +@JsonGen(publicConverter = false) +public class DohRecord { + private JsonObject json; + + public DohRecord() { + } + + public DohRecord(JsonObject json) { + DohRecordConverter.fromJson(json, this); + this.json = json.copy(); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + DohRecordConverter.toJson(this, json); + return json; + } + + private int status; + + private boolean tc; + + private boolean rd; + + private boolean ra; + + private boolean ad; + + private boolean cd; + + private List question; + + private List answer; + private List authority; + private List additional; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isTc() { + return tc; + } + + public void setTc(boolean tc) { + this.tc = tc; + } + + public boolean isRd() { + return rd; + } + + public void setRd(boolean rd) { + this.rd = rd; + } + + public boolean isRa() { + return ra; + } + + public void setRa(boolean ra) { + this.ra = ra; + } + + public boolean isAd() { + return ad; + } + + public void setAd(boolean ad) { + this.ad = ad; + } + + public boolean isCd() { + return cd; + } + + public void setCd(boolean cd) { + this.cd = cd; + } + + public List getQuestion() { + return question; + } + + public void setQuestion(List question) { + this.question = question; + } + + public List getAnswer() { + return answer; + } + + public void setAnswer(List answer) { + this.answer = answer; + } + + public List getAuthority() { + return authority; + } + + public void setAuthority(List authority) { + this.authority = authority; + } + + public List getAdditional() { + return additional; + } + + public void setAdditional(List additional) { + this.additional = additional; + } +} + + diff --git a/src/main/java/io/vertx/core/dns/dnsrecord/DohResourceRecord.java b/src/main/java/io/vertx/core/dns/dnsrecord/DohResourceRecord.java new file mode 100644 index 00000000000..976936fea0c --- /dev/null +++ b/src/main/java/io/vertx/core/dns/dnsrecord/DohResourceRecord.java @@ -0,0 +1,64 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +/** + * @author Iman Zolfaghari + */ + +@DataObject +@JsonGen(publicConverter = false) +public class DohResourceRecord { + private JsonObject json; + + public DohResourceRecord() { + } + + public DohResourceRecord(JsonObject json) { + DohResourceRecordConverter.fromJson(json, DohResourceRecord.this); + this.json = json.copy(); + } + public JsonObject toJson() { + JsonObject json = new JsonObject(); + DohResourceRecordConverter.toJson(this, json); + return json; + } + private String name; + private int type; + private int ttl; + private String data; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getTtl() { + return ttl; + } + + public void setTtl(int ttl) { + this.ttl = ttl; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/src/main/java/io/vertx/core/dns/dnsrecord/Question.java b/src/main/java/io/vertx/core/dns/dnsrecord/Question.java new file mode 100644 index 00000000000..8603e0d5cbd --- /dev/null +++ b/src/main/java/io/vertx/core/dns/dnsrecord/Question.java @@ -0,0 +1,48 @@ +package io.vertx.core.dns.dnsrecord; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +/** + * @author Iman Zolfaghari + */ + +@DataObject +@JsonGen(publicConverter = false) +public class Question { + private JsonObject json; + + public Question() { + } + + public Question(JsonObject json) { + QuestionConverter.fromJson(json, this); + this.json = json.copy(); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + QuestionConverter.toJson(this, json); + return json; + } + + private String name; + private int type; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } +} diff --git a/src/main/java/io/vertx/core/dns/impl/BaseDnsClientImpl.java b/src/main/java/io/vertx/core/dns/impl/BaseDnsClientImpl.java new file mode 100644 index 00000000000..e09735e09f8 --- /dev/null +++ b/src/main/java/io/vertx/core/dns/impl/BaseDnsClientImpl.java @@ -0,0 +1,201 @@ +package io.vertx.core.dns.impl; + +import io.netty.handler.codec.dns.DnsRecordType; +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.dns.DnsClient; +import io.vertx.core.dns.DnsClientOptions; +import io.vertx.core.dns.MxRecord; +import io.vertx.core.dns.SrvRecord; +import io.vertx.core.impl.ContextInternal; +import io.vertx.core.impl.VertxInternal; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +/** + * + * Base class for both DnsClient and DohClient, containing some common methods shared between them. + * + * @author Iman Zolfaghari + */ + + +public abstract class BaseDnsClientImpl implements DnsClient { + private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray(); + protected DnsClientOptions options; + protected VertxInternal vertx; + protected ContextInternal context; + protected volatile Future closed; + + protected abstract Future> lookupList(String name, DnsRecordType... types); + + @Override + public DnsClient lookup4(String name, Handler> handler) { + lookup4(name).onComplete(handler); + return this; + } + + @Override + public Future<@Nullable String> lookup4(String name) { + return lookupSingle(name, DnsRecordType.A); + } + + @Override + public DnsClient lookup6(String name, Handler> handler) { + lookup6(name).onComplete(handler); + return this; + } + + @Override + public Future<@Nullable String> lookup6(String name) { + return lookupSingle(name, DnsRecordType.AAAA); + } + + @Override + public DnsClient lookup(String name, Handler> handler) { + lookup(name).onComplete(handler); + return this; + } + + @Override + public Future<@Nullable String> lookup(String name) { + return lookupSingle(name, DnsRecordType.A, DnsRecordType.AAAA); + } + + @Override + public DnsClient resolveA(String name, Handler>> handler) { + resolveA(name).onComplete(handler); + return this; + } + + @Override + public Future> resolveA(String name) { + return lookupList(name, DnsRecordType.A); + } + + @Override + public DnsClient resolveCNAME(String name, Handler >> handler) { + resolveCNAME(name).onComplete(handler); + return this; + } + + @Override + public Future> resolveCNAME(String name) { + return lookupList(name, DnsRecordType.CNAME); + } + + @Override + public DnsClient resolveMX(String name, Handler>> handler) { + resolveMX(name).onComplete(handler); + return this; + } + + @Override + public Future> resolveMX(String name) { + return lookupList(name, DnsRecordType.MX); + } + + @Override + public DnsClient resolveTXT(String name, Handler>> handler) { + resolveTXT(name).onComplete(handler); + return this; + } + + @Override + public Future<@Nullable String> resolvePTR(String name) { + return lookupSingle(name, DnsRecordType.PTR); + } + + @Override + public DnsClient resolvePTR(String name, Handler> handler) { + resolvePTR(name).onComplete(handler); + return this; + } + + @Override + public DnsClient resolveAAAA(String name, Handler>> handler) { + resolveAAAA(name).onComplete(handler); + return this; + } + + @Override + public Future> resolveAAAA(String name) { + return lookupList(name, DnsRecordType.AAAA); + } + + @Override + public Future> resolveNS(String name) { + return lookupList(name, DnsRecordType.NS); + } + + @Override + public DnsClient resolveNS(String name, Handler>> handler) { + resolveNS(name).onComplete(handler); + return this; + } + + @Override + public Future> resolveSRV(String name) { + return lookupList(name, DnsRecordType.SRV); + } + + @Override + public DnsClient resolveSRV(String name, Handler>> handler) { + resolveSRV(name).onComplete(handler); + return this; + } + + @Override + public Future<@Nullable String> reverseLookup(String address) { + try { + InetAddress inetAddress = InetAddress.getByName(address); + byte[] addr = inetAddress.getAddress(); + + StringBuilder reverseName = new StringBuilder(64); + if (inetAddress instanceof Inet4Address) { + // reverse ipv4 address + reverseName.append(addr[3] & 0xff).append(".") + .append(addr[2]& 0xff).append(".") + .append(addr[1]& 0xff).append(".") + .append(addr[0]& 0xff); + } else { + // It is an ipv 6 address time to reverse it + for (int i = 0; i < 16; i++) { + reverseName.append(HEX_TABLE[(addr[15 - i] & 0xf)]); + reverseName.append("."); + reverseName.append(HEX_TABLE[(addr[15 - i] >> 4) & 0xf]); + if (i != 15) { + reverseName.append("."); + } + } + } + reverseName.append(".in-addr.arpa"); + + return resolvePTR(reverseName.toString()); + } catch (UnknownHostException e) { + // Should never happen as we work with ip addresses as input + // anyway just in case notify the handler + return Future.failedFuture(e); + } + } + + @Override + public DnsClient reverseLookup(String address, Handler> handler) { + reverseLookup(address).onComplete(handler); + return this; + } + + private Future lookupSingle(String name, DnsRecordType... types) { + return this.lookupList(name, types).map(result -> result.isEmpty() ? null : result.get(0)); + } + + @Override + public void close(Handler> handler) { + close().onComplete(handler); + } +} diff --git a/src/main/java/io/vertx/core/dns/impl/DnsClientImpl.java b/src/main/java/io/vertx/core/dns/impl/DnsClientImpl.java index 4af46d05a4d..7947f65f905 100644 --- a/src/main/java/io/vertx/core/dns/impl/DnsClientImpl.java +++ b/src/main/java/io/vertx/core/dns/impl/DnsClientImpl.java @@ -19,22 +19,23 @@ import io.netty.handler.logging.LoggingHandler; import io.netty.util.collection.LongObjectHashMap; import io.netty.util.collection.LongObjectMap; -import io.vertx.codegen.annotations.Nullable; -import io.vertx.core.*; -import io.vertx.core.dns.*; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.VertxException; +import io.vertx.core.buffer.impl.PartialPooledByteBufAllocator; +import io.vertx.core.dns.DnsClientOptions; +import io.vertx.core.dns.DnsException; import io.vertx.core.dns.DnsResponseCode; +import io.vertx.core.dns.impl.decoder.JsonHelper; import io.vertx.core.dns.impl.decoder.RecordDecoder; import io.vertx.core.impl.ContextInternal; -import io.vertx.core.impl.future.PromiseInternal; import io.vertx.core.impl.VertxInternal; -import io.vertx.core.buffer.impl.PartialPooledByteBufAllocator; +import io.vertx.core.impl.future.PromiseInternal; import io.vertx.core.net.impl.ConnectionBase; import io.vertx.core.spi.transport.Transport; import java.net.Inet4Address; -import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,17 +45,11 @@ /** * @author Norman Maurer */ -public final class DnsClientImpl implements DnsClient { +public final class DnsClientImpl extends BaseDnsClientImpl { - private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray(); - - private final VertxInternal vertx; - private final LongObjectMap inProgressMap = new LongObjectHashMap<>(); + private final LongObjectMap inProgressMap = new LongObjectHashMap<>(); private final InetSocketAddress dnsServer; - private final ContextInternal context; private final DatagramChannel channel; - private final DnsClientOptions options; - private volatile Future closed; public DnsClientImpl(VertxInternal vertx, DnsClientOptions options) { Objects.requireNonNull(options, "no null options accepted"); @@ -70,7 +65,8 @@ public DnsClientImpl(VertxInternal vertx, DnsClientOptions options) { Transport transport = vertx.transport(); context = vertx.getOrCreateContext(); - channel = transport.datagramChannel(this.dnsServer.getAddress() instanceof Inet4Address ? InternetProtocolFamily.IPv4 : InternetProtocolFamily.IPv6); + channel = transport.datagramChannel(this.dnsServer.getAddress() instanceof Inet4Address ? + InternetProtocolFamily.IPv4 : InternetProtocolFamily.IPv6); channel.config().setOption(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true); MaxMessagesRecvByteBufAllocator bufAllocator = channel.config().getRecvByteBufAllocator(); bufAllocator.maxMessagesPerRead(1); @@ -93,179 +89,9 @@ protected void channelRead0(ChannelHandlerContext ctx, DnsResponse msg) { }); } - @Override - public DnsClient lookup4(String name, Handler> handler) { - lookup4(name).onComplete(handler); - return this; - } - - @Override - public Future<@Nullable String> lookup4(String name) { - return lookupSingle(name, DnsRecordType.A); - } - - @Override - public DnsClient lookup6(String name, Handler> handler) { - lookup6(name).onComplete(handler); - return this; - } - - @Override - public Future<@Nullable String> lookup6(String name) { - return lookupSingle(name, DnsRecordType.AAAA); - } - - @Override - public DnsClient lookup(String name, Handler> handler) { - lookup(name).onComplete(handler); - return this; - } - - @Override - public Future<@Nullable String> lookup(String name) { - return lookupSingle(name, DnsRecordType.A, DnsRecordType.AAAA); - } - - @Override - public DnsClient resolveA(String name, Handler>> handler) { - resolveA(name).onComplete(handler); - return this; - } - - @Override - public Future> resolveA(String name) { - return lookupList(name, DnsRecordType.A); - } - - @Override - public DnsClient resolveCNAME(String name, Handler >> handler) { - resolveCNAME(name).onComplete(handler); - return this; - } - - @Override - public Future> resolveCNAME(String name) { - return lookupList(name, DnsRecordType.CNAME); - } - - @Override - public DnsClient resolveMX(String name, Handler>> handler) { - resolveMX(name).onComplete(handler); - return this; - } - - @Override - public Future> resolveMX(String name) { - return lookupList(name, DnsRecordType.MX); - } - - @Override - public Future> resolveTXT(String name) { - return this.>lookupList(name, DnsRecordType.TXT).map(records -> { - List txts = new ArrayList<>(); - for (List txt: records) { - txts.addAll(txt); - } - return txts; - }); - } - - @Override - public DnsClient resolveTXT(String name, Handler>> handler) { - resolveTXT(name).onComplete(handler); - return this; - } - - @Override - public Future<@Nullable String> resolvePTR(String name) { - return lookupSingle(name, DnsRecordType.PTR); - } - - @Override - public DnsClient resolvePTR(String name, Handler> handler) { - resolvePTR(name).onComplete(handler); - return this; - } - - @Override - public DnsClient resolveAAAA(String name, Handler>> handler) { - resolveAAAA(name).onComplete(handler); - return this; - } - - @Override - public Future> resolveAAAA(String name) { - return lookupList(name, DnsRecordType.AAAA); - } - - @Override - public Future> resolveNS(String name) { - return lookupList(name, DnsRecordType.NS); - } - - @Override - public DnsClient resolveNS(String name, Handler>> handler) { - resolveNS(name).onComplete(handler); - return this; - } - - @Override - public Future> resolveSRV(String name) { - return lookupList(name, DnsRecordType.SRV); - } - - @Override - public DnsClient resolveSRV(String name, Handler>> handler) { - resolveSRV(name).onComplete(handler); - return this; - } - - @Override - public Future<@Nullable String> reverseLookup(String address) { - try { - InetAddress inetAddress = InetAddress.getByName(address); - byte[] addr = inetAddress.getAddress(); - - StringBuilder reverseName = new StringBuilder(64); - if (inetAddress instanceof Inet4Address) { - // reverse ipv4 address - reverseName.append(addr[3] & 0xff).append(".") - .append(addr[2]& 0xff).append(".") - .append(addr[1]& 0xff).append(".") - .append(addr[0]& 0xff); - } else { - // It is an ipv 6 address time to reverse it - for (int i = 0; i < 16; i++) { - reverseName.append(HEX_TABLE[(addr[15 - i] & 0xf)]); - reverseName.append("."); - reverseName.append(HEX_TABLE[(addr[15 - i] >> 4) & 0xf]); - if (i != 15) { - reverseName.append("."); - } - } - } - reverseName.append(".in-addr.arpa"); - - return resolvePTR(reverseName.toString()); - } catch (UnknownHostException e) { - // Should never happen as we work with ip addresses as input - // anyway just in case notify the handler - return Future.failedFuture(e); - } - } - - @Override - public DnsClient reverseLookup(String address, Handler> handler) { - reverseLookup(address).onComplete(handler); - return this; - } - - private Future lookupSingle(String name, DnsRecordType... types) { - return this.lookupList(name, types).map(result -> result.isEmpty() ? null : result.get(0)); - } @SuppressWarnings("unchecked") - private Future> lookupList(String name, DnsRecordType... types) { + protected Future> lookupList(String name, DnsRecordType... types) { ContextInternal ctx = vertx.getOrCreateContext(); if (closed != null) { return ctx.failedFuture(ConnectionBase.CLOSED_EXCEPTION); @@ -303,11 +129,10 @@ private class Query { long timerID; public Query(String name, DnsRecordType[] types) { - this.msg = new DatagramDnsQuery(null, dnsServer, ThreadLocalRandom.current().nextInt()).setRecursionDesired(options.isRecursionDesired()); - if (!name.endsWith(".")) { - name += "."; - } - for (DnsRecordType type: types) { + this.msg = + new DatagramDnsQuery(null, dnsServer, ThreadLocalRandom.current().nextInt()).setRecursionDesired(options.isRecursionDesired()); + name = JsonHelper.appendDotIfRequired(name); + for (DnsRecordType type : types) { msg.addRecord(DnsSection.QUESTION, new DefaultDnsQuestion(name, type, DnsRecord.CLASS_IN)); } this.promise = context.nettyEventLoop().newPromise(); @@ -332,7 +157,7 @@ void handle(DnsResponse msg) { } int count = msg.count(DnsSection.ANSWER); List records = new ArrayList<>(count); - for (int idx = 0;idx < count;idx++) { + for (int idx = 0; idx < count; idx++) { DnsRecord a = msg.recordAt(DnsSection.ANSWER, idx); T record; try { @@ -381,8 +206,14 @@ private boolean isRequestedType(DnsRecordType dnsRecordType, DnsRecordType[] typ } @Override - public void close(Handler> handler) { - close().onComplete(handler); + public Future> resolveTXT(String name) { + return this.>lookupList(name, DnsRecordType.TXT).map(records -> { + List txts = new ArrayList<>(); + for (List txt: records) { + txts.addAll(txt); + } + return txts; + }); } @Override diff --git a/src/main/java/io/vertx/core/dns/impl/DohClientImpl.java b/src/main/java/io/vertx/core/dns/impl/DohClientImpl.java new file mode 100644 index 00000000000..5e4074e7fbe --- /dev/null +++ b/src/main/java/io/vertx/core/dns/impl/DohClientImpl.java @@ -0,0 +1,210 @@ +package io.vertx.core.dns.impl; + +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.dns.DnsRecordType; +import io.vertx.core.*; +import io.vertx.core.dns.DnsClientOptions; +import io.vertx.core.dns.DnsException; +import io.vertx.core.dns.DnsResponseCode; +import io.vertx.core.dns.dnsrecord.DohRecord; +import io.vertx.core.dns.dnsrecord.DohResourceRecord; +import io.vertx.core.dns.impl.decoder.DohRecordDecoder; +import io.vertx.core.dns.impl.decoder.JsonHelper; +import io.vertx.core.http.*; +import io.vertx.core.impl.ContextInternal; +import io.vertx.core.impl.VertxInternal; +import io.vertx.core.json.JsonObject; +import io.vertx.core.net.impl.ConnectionBase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import static io.vertx.core.dns.impl.decoder.JsonHelper.appendDotIfRequired; + +/** + * @author Iman Zolfaghari + */ + +public class DohClientImpl extends BaseDnsClientImpl { + public static final String DOH_URL_FORMAT = "/dns-query?name=%s&type=%s"; + private final HttpClient httpClient; + private final ConcurrentMap> inProgressMap = new ConcurrentHashMap<>(); + + public DohClientImpl(VertxInternal vertx, DnsClientOptions options) { + Objects.requireNonNull(options, "no null options accepted"); + Objects.requireNonNull(options.getHost(), "no null host accepted"); + + this.options = new DnsClientOptions(options); + + this.vertx = vertx; + this.context = vertx.getOrCreateContext(); + + HttpClientOptions httpClientOptions = new HttpClientOptions() + .setLogActivity(options.getLogActivity()) + .setActivityLogDataFormat(options.getActivityLogFormat()) + .setSsl(true) + .setUseAlpn(true) + .setTrustAll(true) + .setVerifyHost(false) + .setProtocolVersion(HttpVersion.HTTP_2); + + this.httpClient = vertx.createHttpClient(httpClientOptions); + } + + protected Future> lookupList(String name, DnsRecordType... types) { + ContextInternal ctx = vertx.getOrCreateContext(); + if (closed != null) { + return ctx.failedFuture(ConnectionBase.CLOSED_EXCEPTION); + } + Objects.requireNonNull(name, "no null name accepted"); + + Query query = new Query<>(name, types); + return query.run(); + } + + private class Query { + final Promise> promise; + final String name; + final DnsRecordType[] types; + long timerID; + int id; + + public Query(String name, DnsRecordType[] types) { + this.types = types; + this.promise = Promise.promise(); + this.name = appendDotIfRequired(name); + this.id = ThreadLocalRandom.current().nextInt(); + } + + public Future> run() { + inProgressMap.put(id, this); + timerID = vertx.setTimer(options.getQueryTimeout(), id -> { + timerID = -1; + context.runOnContext(v -> fail(new VertxException("DNS query timeout for " + name))); + }); + + List>> futures = Arrays.stream(types).map(this::fetchDns).collect(Collectors.toList()); + + Future.all(futures) + .onSuccess(compositeFuture -> promise.tryComplete(combineCompositeFutureResults(compositeFuture))) + .onFailure(t -> context.emit(t, this::fail)); + + return promise.future(); + } + + private Future> fetchDns(DnsRecordType type) { + RequestOptions requestOptions = new RequestOptions() + .setSsl(true) + .setPort(options.getPort()) + .setHost(options.getHost()) + .setURI(String.format(DOH_URL_FORMAT, name, type.name())) + .setMethod(HttpMethod.GET) + .addHeader("accept", "application/dns-json"); + + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .compose(HttpClientResponse::body) + .map(buf -> new DohRecord(JsonHelper.normalizePropertyNames(new JsonObject(buf)))) + .compose(this::handleResolvedDns); + } + + private List combineCompositeFutureResults(CompositeFuture compositeFuture) { + List result = new ArrayList<>(); + for (int i = 0; i < compositeFuture.size(); i++) { + if (compositeFuture.resultAt(i) instanceof List) { + result.addAll(compositeFuture.>resultAt(i)); + } else { + result.add(compositeFuture.resultAt(i)); + } + } + return result; + } + + private Future> handleResolvedDns(DohRecord resolvedDns) { + DnsResponseCode code = DnsResponseCode.valueOf(resolvedDns.getStatus()); + + inProgressMap.remove(id); + if (timerID >= 0) { + vertx.cancelTimer(timerID); + } + + if (code != DnsResponseCode.NOERROR) { + fail(new DnsException(code)); + return promise.future(); + } + + List result = new ArrayList<>(); + + if (resolvedDns.getAnswer() != null) { + for (DohResourceRecord answer : resolvedDns.getAnswer()) { + T decoded = decodeResourceRecord(answer); + if (decoded == null) { + return promise.future(); + } + if (isRequestedType(DnsRecordType.valueOf(answer.getType()), types)) { + result.add(decoded); + } + } + } + if (resolvedDns.getAuthority() != null) { + for (DohResourceRecord authority : resolvedDns.getAuthority()) { + result.add(DohRecordDecoder.decode(authority)); + } + } + + return Future.succeededFuture(result); + } + + private T decodeResourceRecord(DohResourceRecord resourceRecord) { + try { + return DohRecordDecoder.decode(resourceRecord); + } catch (DecoderException e) { + fail(e); + } + return null; + } + + private void fail(Throwable cause) { + inProgressMap.remove(id); + if (timerID >= 0) { + vertx.cancelTimer(timerID); + } + promise.tryFail(cause); + } + + private boolean isRequestedType(DnsRecordType dnsRecordType, DnsRecordType[] types) { + List dnsRecordTypes = Arrays.asList(types); +// if(dnsRecordTypes.contains(DnsRecordType.SRV) && dnsRecordType == DnsRecordType.SOA) { +// return true; +// } + return dnsRecordTypes.contains(dnsRecordType); + } + } + + public void inProgressQueries(Handler handler) { + context.runOnContext(v -> handler.handle(inProgressMap.size())); + } + + @Override + public Future> resolveTXT(String name) { + return this.lookupList(name, DnsRecordType.TXT); + } + + @Override + public Future close() { + synchronized (this) { + if (closed != null) { + return closed; + } + closed = Future.succeededFuture(); + } + inProgressMap.values().forEach(query -> query.fail(ConnectionBase.CLOSED_EXCEPTION)); + return closed; + } +} diff --git a/src/main/java/io/vertx/core/dns/impl/decoder/DohRecordDecoder.java b/src/main/java/io/vertx/core/dns/impl/decoder/DohRecordDecoder.java new file mode 100644 index 00000000000..3a601c63c3a --- /dev/null +++ b/src/main/java/io/vertx/core/dns/impl/decoder/DohRecordDecoder.java @@ -0,0 +1,110 @@ +package io.vertx.core.dns.impl.decoder; + +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.dns.DnsRecordType; +import io.vertx.core.dns.dnsrecord.DohResourceRecord; +import io.vertx.core.dns.impl.MxRecordImpl; +import io.vertx.core.dns.impl.SrvRecordImpl; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * + * Handles the decoding of DoH records. + * + * @author Iman Zolfaghari + */ +public class DohRecordDecoder { + private static final Logger log = LoggerFactory.getLogger(DohRecordDecoder.class); + + + /** + * Decodes MX (mail exchanger) resource records. + */ + public static final Function MX = dohResourceRecord -> { + String[] parts = dohResourceRecord.getData().replaceAll("[\\[\\]]", "").split(" ", 2); + int priority = Integer.parseInt(parts[0]); + String name = parts[1]; + return new MxRecordImpl(priority, name); + }; + + /** + * Decodes SOA (start of authority) resource records. + */ + public static final Function SOA = dohResourceRecord -> { + String[] parts = dohResourceRecord.getData().split(" "); + String mName = parts[0]; + String rName = parts[1]; + long serial = Long.parseLong(parts[2]); + int refresh = Integer.parseInt(parts[3]); + int retry = Integer.parseInt(parts[4]); + int expire = Integer.parseInt(parts[5]); + long minimum = Long.parseLong(parts[6]); + + return new StartOfAuthorityRecord(mName, rName, serial, refresh, retry, expire, minimum); + }; + + + /** + * Decodes SRV (service) resource records. + */ + public static final Function SRV = dohResourceRecord -> { + String[] parts = dohResourceRecord.getData().split(" "); + int priority = Integer.parseInt(parts[0]); + int weight = Integer.parseInt(parts[1]); + int port = Integer.parseInt(parts[2]); + String name = parts[3]; + String protocol = parts[4]; + String service = parts[5]; + String target = parts[6]; + + return new SrvRecordImpl(priority, weight, port, name, protocol, service, target); + }; + + /** + * Decodes default resource records to extract and return the data property. + */ + public static final Function DEFAULT = DohResourceRecord::getData; + private static final Map> decoders = new HashMap<>(); + + static { + decoders.put(DnsRecordType.MX, DohRecordDecoder.MX); + decoders.put(DnsRecordType.SRV, DohRecordDecoder.SRV); + decoders.put(DnsRecordType.SOA, DohRecordDecoder.SOA); + + decoders.put(DnsRecordType.A, DohRecordDecoder.DEFAULT); + decoders.put(DnsRecordType.AAAA, DohRecordDecoder.DEFAULT); + decoders.put(DnsRecordType.TXT, DohRecordDecoder.DEFAULT); + decoders.put(DnsRecordType.NS, DohRecordDecoder.DEFAULT); + decoders.put(DnsRecordType.CNAME, DohRecordDecoder.DEFAULT); + decoders.put(DnsRecordType.PTR, DohRecordDecoder.DEFAULT); + } + + /** + * Decodes a resource record and returns the result. + * + * @param record + * @return the decoded resource record + */ + + @SuppressWarnings("unchecked") + public static T decode(DohResourceRecord record) { + DnsRecordType type = DnsRecordType.valueOf(record.getType()); + Function decoder = decoders.get(type); + if (decoder == null) { + throw new DecoderException("DNS record decoding error occurred: Unsupported resource record type [id: " + type + "]."); + } + T result = null; + try { + result = (T) decoder.apply(record); + } catch (Exception e) { + log.error(e.getMessage(), e.getCause()); + } + return result; + } + +} diff --git a/src/main/java/io/vertx/core/dns/impl/decoder/JsonHelper.java b/src/main/java/io/vertx/core/dns/impl/decoder/JsonHelper.java new file mode 100644 index 00000000000..517d58d02bd --- /dev/null +++ b/src/main/java/io/vertx/core/dns/impl/decoder/JsonHelper.java @@ -0,0 +1,41 @@ +package io.vertx.core.dns.impl.decoder; + +import io.vertx.codegen.Helper; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * @author Iman Zolfaghari + */ + +public class JsonHelper { + public static T normalizePropertyNames(T obj) { + if (obj instanceof JsonObject) { + JsonObject json = new JsonObject(); + ((JsonObject) obj).forEach(member -> json.put(normalizePropertyName(member.getKey()), normalizePropertyNames(member.getValue()))); + return (T) json; + } + + if (obj instanceof JsonArray) { + JsonArray json = new JsonArray(); + ((JsonArray) obj).forEach(item -> json.add(normalizePropertyNames(item))); + return (T) json; + } + + return obj; + } + + private static String normalizePropertyName(String text) { + if(text == null || text.isEmpty()) + return text; + return Helper.normalizePropertyName(text); + } + + public static String appendDotIfRequired(String name) { + if (!name.endsWith(".")) { + name += "."; + } + return name; + } + +} diff --git a/src/main/java/io/vertx/core/dns/impl/decoder/StartOfAuthorityRecord.java b/src/main/java/io/vertx/core/dns/impl/decoder/StartOfAuthorityRecord.java index 5b8c2c367e5..9da9321102d 100644 --- a/src/main/java/io/vertx/core/dns/impl/decoder/StartOfAuthorityRecord.java +++ b/src/main/java/io/vertx/core/dns/impl/decoder/StartOfAuthorityRecord.java @@ -116,4 +116,16 @@ public long minimumTtl() { return minimumTtl; } + @Override + public String toString() { + return String.join(" ", + primaryNameServer(), + responsiblePerson(), + String.valueOf(serial()), + String.valueOf(refreshTime()), + String.valueOf(retryTime()), + String.valueOf(expireTime()), + String.valueOf(minimumTtl()) + ); + } } diff --git a/src/main/java/io/vertx/core/impl/VertxImpl.java b/src/main/java/io/vertx/core/impl/VertxImpl.java index 1fed3fef477..589759ed469 100644 --- a/src/main/java/io/vertx/core/impl/VertxImpl.java +++ b/src/main/java/io/vertx/core/impl/VertxImpl.java @@ -17,8 +17,8 @@ import io.netty.util.ResourceLeakDetector; import io.netty.util.concurrent.GenericFutureListener; import io.vertx.core.Future; -import io.vertx.core.*; import io.vertx.core.Timer; +import io.vertx.core.*; import io.vertx.core.datagram.DatagramSocket; import io.vertx.core.datagram.DatagramSocketOptions; import io.vertx.core.datagram.impl.DatagramSocketImpl; @@ -26,33 +26,30 @@ import io.vertx.core.dns.DnsClient; import io.vertx.core.dns.DnsClientOptions; import io.vertx.core.dns.impl.DnsClientImpl; +import io.vertx.core.dns.impl.DohClientImpl; import io.vertx.core.eventbus.EventBus; import io.vertx.core.eventbus.impl.EventBusImpl; import io.vertx.core.eventbus.impl.EventBusInternal; import io.vertx.core.eventbus.impl.clustered.ClusteredEventBus; import io.vertx.core.file.FileSystem; +import io.vertx.core.file.impl.FileSystemImpl; +import io.vertx.core.file.impl.WindowsFileSystem; import io.vertx.core.http.*; import io.vertx.core.http.impl.*; import io.vertx.core.impl.btc.BlockedThreadChecker; -import io.vertx.core.net.impl.NetClientBuilder; -import io.vertx.core.impl.transports.JDKTransport; -import io.vertx.core.spi.file.FileResolver; -import io.vertx.core.file.impl.FileSystemImpl; -import io.vertx.core.file.impl.WindowsFileSystem; -import io.vertx.core.http.impl.HttpClientImpl; -import io.vertx.core.http.impl.HttpServerImpl; import io.vertx.core.impl.future.PromiseInternal; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.impl.resolver.DnsResolverProvider; +import io.vertx.core.impl.transports.JDKTransport; import io.vertx.core.net.NetClient; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetServerOptions; +import io.vertx.core.net.impl.NetClientBuilder; import io.vertx.core.net.impl.NetServerImpl; import io.vertx.core.net.impl.ServerID; import io.vertx.core.net.impl.TCPServerBase; -import io.vertx.core.spi.transport.Transport; import io.vertx.core.shareddata.SharedData; import io.vertx.core.shareddata.impl.SharedDataImpl; import io.vertx.core.spi.ExecutorServiceFactory; @@ -60,11 +57,13 @@ import io.vertx.core.spi.VertxThreadFactory; import io.vertx.core.spi.cluster.ClusterManager; import io.vertx.core.spi.cluster.NodeSelector; +import io.vertx.core.spi.file.FileResolver; import io.vertx.core.spi.metrics.Metrics; import io.vertx.core.spi.metrics.MetricsProvider; import io.vertx.core.spi.metrics.PoolMetrics; import io.vertx.core.spi.metrics.VertxMetrics; import io.vertx.core.spi.tracing.VertxTracer; +import io.vertx.core.spi.transport.Transport; import java.io.File; import java.io.IOException; @@ -96,6 +95,7 @@ public class VertxImpl implements VertxInternal, MetricsProvider { private static final String CLUSTER_MAP_NAME = "__vertx.haInfo"; private static final String NETTY_IO_RATIO_PROPERTY_NAME = "vertx.nettyIORatio"; private static final int NETTY_IO_RATIO = Integer.getInteger(NETTY_IO_RATIO_PROPERTY_NAME, 50); + public static final String DEFAULT_DOH_HOST = "1.1.1.1"; // Not cached for graalvm private static ThreadFactory virtualThreadFactory() { @@ -632,19 +632,35 @@ public DnsClient createDnsClient() { return createDnsClient(new DnsClientOptions()); } + @Override + public DnsClient createDohClient(String host) { + return createDnsClient(new DnsClientOptions().setSsl(true).setPort(443).setHost(host)); + } + + @Override + public DnsClient createDohClient() { + return createDnsClient(new DnsClientOptions().setSsl(true)); + } + @Override public DnsClient createDnsClient(DnsClientOptions options) { String host = options.getHost(); int port = options.getPort(); if (host == null || port < 0) { - DnsResolverProvider provider = DnsResolverProvider.create(this, addressResolverOptions); - InetSocketAddress address = provider.nameServerAddresses().get(0); - // provide the host and port - options = new DnsClientOptions(options) - .setHost(address.getAddress().getHostAddress()) - .setPort(address.getPort()); - } - return new DnsClientImpl(this, options); + if (options.isSsl()) { + options = new DnsClientOptions(options) + .setHost(DEFAULT_DOH_HOST) + .setPort(443); + } else { + DnsResolverProvider provider = DnsResolverProvider.create(this, addressResolverOptions); + InetSocketAddress address = provider.nameServerAddresses().get(0); + // provide the host and port + options = new DnsClientOptions(options) + .setHost(address.getAddress().getHostAddress()) + .setPort(address.getPort()); + } + } + return options.isSsl() ? new DohClientImpl(this, options) : new DnsClientImpl(this, options); } private long scheduleTimeout(ContextInternal context, diff --git a/src/main/java/io/vertx/core/impl/VertxWrapper.java b/src/main/java/io/vertx/core/impl/VertxWrapper.java index 54a17a0dbf2..71c7d159e68 100644 --- a/src/main/java/io/vertx/core/impl/VertxWrapper.java +++ b/src/main/java/io/vertx/core/impl/VertxWrapper.java @@ -44,10 +44,8 @@ import java.net.InetSocketAddress; import java.util.Map; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; -import java.util.function.Function; import java.util.function.Supplier; /** @@ -115,6 +113,16 @@ public DnsClient createDnsClient() { return delegate.createDnsClient(); } + @Override + public DnsClient createDohClient(String host) { + return delegate.createDohClient(host); + } + + @Override + public DnsClient createDohClient() { + return delegate.createDohClient(); + } + @Override public DnsClient createDnsClient(DnsClientOptions options) { return delegate.createDnsClient(options); diff --git a/src/test/java/io/vertx/core/dns/DohTest.java b/src/test/java/io/vertx/core/dns/DohTest.java new file mode 100644 index 00000000000..5c04f157c2a --- /dev/null +++ b/src/test/java/io/vertx/core/dns/DohTest.java @@ -0,0 +1,466 @@ +package io.vertx.core.dns; + +import io.vertx.core.Vertx; +import io.vertx.core.VertxException; +import io.vertx.core.VertxOptions; +import io.vertx.core.dns.impl.DohClientImpl; +import io.vertx.test.core.TestUtils; +import io.vertx.test.core.VertxTestBase; +import io.vertx.test.fakedns.FakeDNSServer; +import io.vertx.test.netty.TestLoggerFactory; +import org.apache.directory.server.dns.messages.DnsMessage; +import org.apache.directory.server.dns.store.RecordStore; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static io.vertx.test.core.TestUtils.assertNullPointerException; + +/** + * @author Iman Zolfaghari + */ +public class DohTest extends VertxTestBase { + + private FakeDNSServer dnsServer; + + @Override + public void setUp() throws Exception { + super.setUp(); + dnsServer = new FakeDNSServer(true, vertx); + dnsServer.start(); + } + + @Override + protected void tearDown() throws Exception { + dnsServer.stop(); + super.tearDown(); + } + + @Test + public void testIllegalArguments() throws Exception { + dnsServer.testResolveAAAA("::1"); + DnsClient dns = prepareDns(); + + assertNullPointerException(() -> dns.lookup(null, ar -> {})); + assertNullPointerException(() -> dns.lookup4(null, ar -> {})); + assertNullPointerException(() -> dns.lookup6(null, ar -> {})); + assertNullPointerException(() -> dns.resolveA(null, ar -> {})); + assertNullPointerException(() -> dns.resolveAAAA(null, ar -> {})); + assertNullPointerException(() -> dns.resolveCNAME(null, ar -> {})); + assertNullPointerException(() -> dns.resolveMX(null, ar -> {})); + assertNullPointerException(() -> dns.resolveTXT(null, ar -> {})); + assertNullPointerException(() -> dns.resolvePTR(null, ar -> {})); + assertNullPointerException(() -> dns.resolveNS(null, ar -> {})); + assertNullPointerException(() -> dns.resolveSRV(null, ar -> {})); + } + + @Test + public void testDefaultDnsClientWithOptions() throws Exception { + int port = 53529; + VertxOptions vertxOptions = new VertxOptions(); + vertxOptions.getAddressResolverOptions().addServer("127.0.0.1" + ":" + port); + + Vertx vertxWithFakeDns = Vertx.vertx(vertxOptions); + + FakeDNSServer dnsServer2 = new FakeDNSServer(true, vertxWithFakeDns); + dnsServer2.port(port); + dnsServer2.start(); + + Function vertxDnsClientFunction = + vertx -> vertx.createDnsClient(new DnsClientOptions().setSsl(true).setPort(dnsServer2.port()).setHost(dnsServer2.ipAddress())); + + DnsClient dnsClient = vertxDnsClientFunction.apply(vertxWithFakeDns); + + final String ip = "10.0.0.1"; + dnsServer2.testLookup4(ip); + + dnsClient.lookup4("vertx.io", onSuccess(result -> { + assertEquals(ip, result); + testComplete(); + })); + await(); + vertxWithFakeDns.close(); + dnsServer2.stop(); + } + + @Test + public void testResolveA() throws Exception { + final String ip = "10.0.0.1"; + dnsServer.testResolveA(ip); + DnsClient dns = prepareDns(); + + dns.resolveA("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals(ip, result.get(0)); + ((DohClientImpl) dns).inProgressQueries(num -> { + assertEquals(0, (int)num); + testComplete(); + }); + })); + await(); + } + + @Test + public void testUnresolvedDnsServer() throws Exception { + try { + DnsClient dns = vertx.createDnsClient(new DnsClientOptions().setHost("iamanunresolvablednsserver.com").setPort(53)); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IllegalArgumentException); + assertEquals("Cant resolve the host to a valid ip address", e.getMessage()); + } + } + + @Test + public void testResolveAIpV6() throws Exception { + final String ip = "10.0.0.1"; + FakeDNSServer dnsServer2 = new FakeDNSServer(true, vertx); + dnsServer2.ipAddress("::1"); + dnsServer2.start(); + dnsServer2.testResolveA(ip).ipAddress("::1"); + // force the fake dns server to Ipv6 + DnsClient dns = + vertx.createDnsClient(new DnsClientOptions().setSsl(true).setPort(dnsServer2.port()).setHost(dnsServer2.ipAddress())); + dns.resolveA("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals(ip, result.get(0)); + ((DohClientImpl) dns).inProgressQueries(num -> { + assertEquals(0, (int) num); + testComplete(); + }); + })); + await(); + dnsServer2.stop(); + } + + @Test + public void testResolveAAAA() throws Exception { + dnsServer.testResolveAAAA("::1"); + DnsClient dns = prepareDns(); + + dns.resolveAAAA("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals("0:0:0:0:0:0:0:1", result.get(0)); + testComplete(); + })); + await(); + } + + @Test + public void testResolveMX() throws Exception { + final String mxRecord = "mail.vertx.io"; + final int prio = 10; + dnsServer.testResolveMX(prio, mxRecord); + DnsClient dns = prepareDns(); + + dns.resolveMX("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + MxRecord record = result.get(0); + assertEquals(prio, record.priority()); + assertEquals(record.name(), mxRecord); + testComplete(); + })); + await(); + } + + @Test + public void testResolveTXT() throws Exception { + final String txt = "vertx is awesome"; + dnsServer.testResolveTXT(txt); + DnsClient dns = prepareDns(); + dns.resolveTXT("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals(txt, result.get(0)); + testComplete(); + })); + await(); + } + + @Test + public void testResolveNS() throws Exception { + final String ns = "ns.vertx.io"; + dnsServer.testResolveNS(ns); + DnsClient dns = prepareDns(); + + dns.resolveNS("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals(ns, result.get(0)); + testComplete(); + })); + await(); + } + + @Test + public void testResolveCNAME() throws Exception { + final String cname = "cname.vertx.io"; + dnsServer.testResolveCNAME(cname); + DnsClient dns = prepareDns(); + + dns.resolveCNAME("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + String record = result.get(0); + assertFalse(record.isEmpty()); + assertEquals(cname, record); + testComplete(); + })); + await(); + } + + @Test + public void testResolvePTR() throws Exception { + final String ptr = "ptr.vertx.io"; + dnsServer.testResolvePTR(ptr); + DnsClient dns = prepareDns(); + + dns.resolvePTR("10.0.0.1.in-addr.arpa", onSuccess(result -> { + assertEquals(ptr, result); + testComplete(); + })); + await(); + } + + @Test + public void testResolveSRV() throws Exception { + final int priority = 10; + final int weight = 1; + final int port = 80; + final String target = "vertx.io"; + + dnsServer.testResolveSRV(priority, weight, port, target); + DnsClient dns = prepareDns(); + + dns.resolveSRV("vertx.io", ar -> { + List result = ar.result(); + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + + SrvRecord record = result.get(0); + + assertEquals(priority, record.priority()); + assertEquals(weight, record.weight()); + assertEquals(port, record.port()); + assertEquals(target, record.target()); + + testComplete(); + }); + await(); + } + + @Test + public void testLookup4() throws Exception { + final String ip = "10.0.0.1"; + dnsServer.testLookup4(ip); + DnsClient dns = prepareDns(); + dns.lookup4("vertx.io", onSuccess(result -> { + assertEquals(ip, result); + DnsMessage msg = dnsServer.pollMessage(); + assertTrue(msg.isRecursionDesired()); + testComplete(); + })); + await(); + } + + @Test + public void testLookup6() throws Exception { + dnsServer.testLookup6(); + DnsClient dns = prepareDns(); + + dns.lookup6("vertx.io", onSuccess(result -> { + assertEquals("0:0:0:0:0:0:0:1", result); + testComplete(); + })); + await(); + } + + @Test + public void testLookup() throws Exception { + String ip = "10.0.0.1"; + dnsServer.testLookup(ip); + DnsClient dns = prepareDns(); + + dns.lookup("vertx.io", onSuccess(result -> { + assertEquals(ip, result); + testComplete(); + })); + await(); + } + + @Test + public void testTimeout() throws Exception { + DnsClient dns = vertx.createDnsClient(new DnsClientOptions().setSsl(true).setHost("localhost").setPort(10000).setQueryTimeout(1)); + + dns.lookup("vertx.io", onFailure(result -> { + assertEquals(VertxException.class, result.getClass()); + assertEquals("DNS query timeout for vertx.io.", result.getMessage()); + ((DohClientImpl) dns).inProgressQueries(num -> { + assertEquals(0, (int)num); + testComplete(); + }); + })); + await(); + } + + @Test + public void testLookupNonExisting() throws Exception { + dnsServer.testLookupNonExisting(); + DnsClient dns = prepareDns(); + dns.lookup("gfegjegjf.sg1", ar -> { + DnsException cause = (DnsException)ar.cause(); + assertEquals(DnsResponseCode.NXDOMAIN, cause.code()); + testComplete(); + }); + await(); + } + + @Test + public void testReverseLookupIpv4() throws Exception { + String address = "10.0.0.1"; + String ptr = "ptr.vertx.io"; + dnsServer.testReverseLookup(ptr); + DnsClient dns = prepareDns(); + + dns.reverseLookup(address, onSuccess(result -> { + assertEquals(ptr, result); + testComplete(); + })); + await(); + } + + @Test + public void testReverseLookupIpv6() throws Exception { + String ptr = "ptr.vertx.io"; + dnsServer.testReverseLookup(ptr); + DnsClient dns = prepareDns(); + + dns.reverseLookup("::1", onSuccess(result -> { + assertEquals(ptr, result); + testComplete(); + })); + await(); + } + + @Test + public void testLookup4CNAME() throws Exception { + final String cname = "cname.vertx.io"; + final String ip = "10.0.0.1"; + dnsServer.testLookup4CNAME(cname, ip); + DnsClient dns = prepareDns(); + + dns.lookup4("vertx.io", onSuccess(result -> { + assertEquals(ip, result); + testComplete(); + })); + await(); + } + + @Test + public void testResolveMXWhenDNSRepliesWithDNAMERecord() throws Exception { + final DnsClient dns = prepareDns(); + dnsServer.testResolveDNAME("mail.vertx.io"); + + dns.resolveMX("vertx.io") + .onComplete(ar -> { + assertTrue(ar.failed()); + testComplete(); + }); + await(); + } + + private TestLoggerFactory testLogging(DnsClientOptions options) { + final String ip = "10.0.0.1"; + dnsServer.testResolveA(ip); + return TestUtils.testLogging(() -> { + try { + prepareDns(options) + .resolveA(ip, fut -> { + testComplete(); + }); + await(); + } catch (Exception e) { + fail(e); + } + }); + } + + @Test + public void testLogActivity() throws Exception { + TestLoggerFactory factory = testLogging(new DnsClientOptions().setSsl(true).setLogActivity(true)); + assertTrue(factory.hasName("io.netty.handler.logging.LoggingHandler")); + } + + @Test + public void testDoNotLogActivity() throws Exception { + TestLoggerFactory factory = testLogging(new DnsClientOptions().setSsl(true).setLogActivity(false)); + assertFalse(factory.hasName("io.netty.handler.logging.LoggingHandler")); + } + + @Test + public void testRecursionDesired() throws Exception { + final String ip = "10.0.0.1"; + + dnsServer.testResolveA(ip); + DnsClient dns = prepareDns(new DnsClientOptions().setSsl(true).setRecursionDesired(true)); + dns.resolveA("vertx.io", onSuccess(result -> { + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + assertEquals(ip, result.get(0)); + DnsMessage msg = dnsServer.pollMessage(); + assertTrue(msg.isRecursionDesired()); + ((DohClientImpl) dns).inProgressQueries(num -> { + assertEquals(0, (int)num); + testComplete(); + }); + })); + await(); + } + + @Test + public void testClose() throws Exception { + waitFor(2); + String ip = "10.0.0.1"; + RecordStore store = dnsServer.testResolveA(ip).store(); + CountDownLatch latch1 = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + dnsServer.store(question -> { + latch1.countDown(); + try { + latch2.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + fail(e); + } + return store.getRecords(question); + }); + DnsClient dns = prepareDns(new DnsClientOptions().setSsl(true).setQueryTimeout(15000)); + dns + .resolveA("vertx.io") + .onComplete(onFailure(timeout -> { + assertTrue(timeout.getMessage().contains("closed")); + complete(); + })); + awaitLatch(latch1); + dns.close().onComplete(onSuccess(v -> { + complete(); + latch2.countDown(); + })); + await(); + } + + private DnsClient prepareDns() throws Exception { + return prepareDns(new DnsClientOptions().setSsl(true).setQueryTimeout(15000)); + } + + private DnsClient prepareDns(DnsClientOptions options) throws Exception { + return vertx.createDnsClient(new DnsClientOptions(options).setPort(dnsServer.port()).setHost(dnsServer.ipAddress())); + } +} diff --git a/src/test/java/io/vertx/test/fakedns/DohMessageEncoder.java b/src/test/java/io/vertx/test/fakedns/DohMessageEncoder.java new file mode 100644 index 00000000000..55ed9ace9e1 --- /dev/null +++ b/src/test/java/io/vertx/test/fakedns/DohMessageEncoder.java @@ -0,0 +1,137 @@ +package io.vertx.test.fakedns; + +import io.vertx.core.dns.MxRecord; +import io.vertx.core.dns.dnsrecord.DohResourceRecord; +import io.vertx.core.dns.impl.MxRecordImpl; +import org.apache.directory.server.dns.messages.RecordType; +import org.apache.directory.server.dns.messages.ResourceRecord; +import org.apache.directory.server.dns.store.DnsAttribute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An encoder for DoH messages. + * + * @author Iman Zolfaghari + */ + +public class DohMessageEncoder { + /** + * the log for this class + */ + private static final Logger log = LoggerFactory.getLogger(DohMessageEncoder.class); + + private static abstract class RecordEncoder { + protected abstract DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord); + + public DohResourceRecord myEncode(ResourceRecord resourceRecord) { + DohResourceRecord dohResourceRecord = new DohResourceRecord(); + dohResourceRecord.setTtl(resourceRecord.getTimeToLive()); + dohResourceRecord.setName(resourceRecord.getDomainName()); + dohResourceRecord.setData(resourceRecord.toString()); + dohResourceRecord.setType(resourceRecord.getRecordType().convert()); + + return encode(dohResourceRecord, resourceRecord); + } + } + + private static final RecordEncoder DEFAULT = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + dohResourceRecord.setData(resourceRecord.get(DnsAttribute.DOMAIN_NAME)); + return dohResourceRecord; + } + }; + private static final RecordEncoder OTHER = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + dohResourceRecord.setData(resourceRecord.get(DnsAttribute.IP_ADDRESS)); + return dohResourceRecord; + } + }; + + private static final RecordEncoder TXT = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + dohResourceRecord.setData(resourceRecord.get(DnsAttribute.CHARACTER_STRING)); + return dohResourceRecord; + } + }; + + private static final RecordEncoder AAAA = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + if (resourceRecord.get(DnsAttribute.IP_ADDRESS).equals("::1")) { + dohResourceRecord.setData("0:0:0:0:0:0:0:1"); + } else { + dohResourceRecord.setData(resourceRecord.get(DnsAttribute.IP_ADDRESS)); + } + return dohResourceRecord; + } + }; + + private static final RecordEncoder MX = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + MxRecord mxRecord = new MxRecordImpl(Integer.parseInt(resourceRecord.get(DnsAttribute.MX_PREFERENCE)), + resourceRecord.get(DnsAttribute.DOMAIN_NAME)); + dohResourceRecord.setData(String.format("[%s]", mxRecord)); + return dohResourceRecord; + } + }; + + private static final RecordEncoder SRV = new RecordEncoder() { + @Override + protected DohResourceRecord encode(DohResourceRecord dohResourceRecord, ResourceRecord resourceRecord) { + dohResourceRecord.setTtl(resourceRecord.getTimeToLive()); + dohResourceRecord.setName(resourceRecord.getDomainName()); + dohResourceRecord.setType(resourceRecord.getRecordType().convert()); + + int priority = Integer.parseInt(resourceRecord.get(DnsAttribute.SERVICE_PRIORITY)); + int weight = Integer.parseInt(resourceRecord.get(DnsAttribute.SERVICE_WEIGHT)); + int port1 = Integer.parseInt(resourceRecord.get(DnsAttribute.SERVICE_PORT)); + String name = "io."; + String protocol = "vertx"; + String service = "dns"; + String target = "vertx.io"; + + dohResourceRecord.setData(String.join(" ", String.valueOf(priority), + String.valueOf(weight), String.valueOf(port1), name, protocol, + service, target)); + + return dohResourceRecord; + } + }; + + + /** + * A Hashed Adapter mapping record types to their encoders. + */ + private static final Map DEFAULT_ENCODERS; + + static { + Map map = new HashMap<>(); + + map.put(RecordType.SOA, DohMessageEncoder.OTHER); + map.put(RecordType.A, DohMessageEncoder.OTHER); + map.put(RecordType.AAAA, DohMessageEncoder.AAAA); + map.put(RecordType.NS, DohMessageEncoder.DEFAULT); + map.put(RecordType.CNAME, DohMessageEncoder.DEFAULT); + map.put(RecordType.PTR, DohMessageEncoder.DEFAULT); + map.put(RecordType.MX, DohMessageEncoder.MX); + map.put(RecordType.SRV, DohMessageEncoder.SRV); + map.put(RecordType.TXT, DohMessageEncoder.TXT); + map.put(RecordType.DNAME, DohMessageEncoder.OTHER); + + DEFAULT_ENCODERS = Collections.unmodifiableMap(map); + } + + public static DohResourceRecord encode(ResourceRecord resourceRecord) { + return DEFAULT_ENCODERS.get(resourceRecord.getRecordType()).myEncode(resourceRecord); + } + +} diff --git a/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java b/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java index ab87564fed4..96c5ea0dfe1 100644 --- a/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java +++ b/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java @@ -11,16 +11,17 @@ package io.vertx.test.fakedns; +import io.netty.handler.codec.dns.DnsRecordType; +import io.vertx.core.Vertx; +import io.vertx.core.dns.DnsResponseCode; +import io.vertx.core.dns.dnsrecord.DohRecord; +import io.vertx.core.dns.dnsrecord.DohResourceRecord; +import io.vertx.core.http.*; +import io.vertx.test.tls.Trust; import org.apache.directory.server.dns.DnsException; import org.apache.directory.server.dns.DnsServer; import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder; -import org.apache.directory.server.dns.messages.DnsMessage; -import org.apache.directory.server.dns.messages.DnsMessageModifier; -import org.apache.directory.server.dns.messages.QuestionRecord; -import org.apache.directory.server.dns.messages.RecordClass; -import org.apache.directory.server.dns.messages.RecordType; -import org.apache.directory.server.dns.messages.ResourceRecord; -import org.apache.directory.server.dns.messages.ResourceRecordModifier; +import org.apache.directory.server.dns.messages.*; import org.apache.directory.server.dns.protocol.DnsProtocolHandler; import org.apache.directory.server.dns.protocol.DnsTcpDecoder; import org.apache.directory.server.dns.protocol.DnsUdpDecoder; @@ -33,11 +34,7 @@ import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.core.session.IoSession; -import org.apache.mina.filter.codec.ProtocolCodecFactory; -import org.apache.mina.filter.codec.ProtocolCodecFilter; -import org.apache.mina.filter.codec.ProtocolDecoder; -import org.apache.mina.filter.codec.ProtocolEncoder; -import org.apache.mina.filter.codec.ProtocolEncoderOutput; +import org.apache.mina.filter.codec.*; import org.apache.mina.transport.socket.DatagramSessionConfig; import java.io.IOException; @@ -48,6 +45,7 @@ /** * @author Norman Maurer + * @author Iman Zolfaghari */ public final class FakeDNSServer extends DnsServer { @@ -88,8 +86,17 @@ public static RecordStore A_store(Function entries) { private volatile RecordStore store; private List acceptors; private final Deque currentMessage = new ArrayDeque<>(); + private final boolean ssl; + private Vertx vertx; + private HttpServer httpServer; public FakeDNSServer() { + this.ssl = false; + } + + public FakeDNSServer(boolean ssl, Vertx vertx) { + this.ssl = ssl; + this.vertx = vertx; } public RecordStore store() { @@ -114,11 +121,19 @@ public FakeDNSServer ipAddress(String ipAddress) { return this; } + public String ipAddress() { + return this.ipAddress; + } + public FakeDNSServer port(int p) { port = p; return this; } + public int port() { + return this.port; + } + public FakeDNSServer testResolveA(final String ipAddress) { return testResolveA(Collections.singletonMap("dns.vertx.io", ipAddress)); } @@ -357,7 +372,7 @@ public FakeDNSServer testLookup4CNAME(final String cname, final String ip) { return store(new RecordStore() { @Override public Set getRecords(QuestionRecord questionRecord) - throws org.apache.directory.server.dns.DnsException { + throws org.apache.directory.server.dns.DnsException { // use LinkedHashSet since the order of the result records has to be preserved to make sure the unit test fails Set set = new LinkedHashSet<>(); @@ -384,6 +399,18 @@ public Set getRecords(QuestionRecord questionRecord) @Override public void start() throws IOException { + if (this.ssl) { + HttpServerOptions options = new HttpServerOptions() + .setSsl(true) + .setPort(port) + .setHost(ipAddress) + .setKeyCertOptions(Trust.DOH_JKS_HOST.get()); + + httpServer = vertx.createHttpServer(options); + httpServer.requestHandler(this::simulateDohServer).listen(); + + return; + } DnsProtocolHandler handler = new DnsProtocolHandler(this, new RecordStore() { @Override @@ -400,9 +427,9 @@ public Set getRecords(QuestionRecord question) throws DnsExcepti public void sessionCreated(IoSession session) throws Exception { // Use our own codec to support AAAA testing if (session.getTransportMetadata().isConnectionless()) { - session.getFilterChain().addFirst( "codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); + session.getFilterChain().addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); } else { - session.getFilterChain().addFirst( "codec", new ProtocolCodecFilter(new TestDnsProtocolTcpCodecFactory())); + session.getFilterChain().addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolTcpCodecFactory())); } } @@ -418,13 +445,13 @@ public void messageReceived(IoSession session, Object message) { }; UdpTransport udpTransport = new UdpTransport(ipAddress, port); - ((DatagramSessionConfig)udpTransport.getAcceptor().getSessionConfig()).setReuseAddress(true); + ((DatagramSessionConfig) udpTransport.getAcceptor().getSessionConfig()).setReuseAddress(true); TcpTransport tcpTransport = new TcpTransport(ipAddress, port); tcpTransport.getAcceptor().getSessionConfig().setReuseAddress(true); setTransports(udpTransport, tcpTransport); - for (Transport transport : getTransports()) { + for (Transport transport : getTransports()) { IoAcceptor acceptor = transport.getAcceptor(); acceptor.setHandler(handler); @@ -434,10 +461,76 @@ public void messageReceived(IoSession session, Object message) { } } + private void simulateDohServer(HttpServerRequest request) { + if (HttpMethod.GET != request.method() || + !request.getHeader("accept").equalsIgnoreCase("application/dns-json")) { + HttpServerResponse response = request.response(); + response.setStatusCode(400); + response.send("DoH Request is not correct!"); + return; + } + + QuestionRecord questionRecord = createQuestionRecord(request); + + cacheDnsMessage(questionRecord); + + Set resourceRecords = getStoredRecords(questionRecord); + List answers = + resourceRecords.stream().map(DohMessageEncoder::encode).collect(Collectors.toList()); + + if (answers.isEmpty()) { + DohRecord dohRecord = new DohRecord(); + dohRecord.setStatus(DnsResponseCode.NXDOMAIN.code()); + sendResponse(dohRecord, request.response()); + return; + } + + DohRecord dohRecord = new DohRecord(); + dohRecord.setStatus(0); + dohRecord.setAnswer(answers); + sendResponse(dohRecord, request.response()); + } + + private QuestionRecord createQuestionRecord(HttpServerRequest request) { + String domainName = request.getParam("name"); + RecordType type = RecordType.convert((short) DnsRecordType.valueOf(request.getParam("type")).intValue()); + return new QuestionRecord(domainName, type, RecordClass.IN); + } + + private void cacheDnsMessage(QuestionRecord questionRecord) { + DnsMessageModifier dnsMessageModifier = new DnsMessageModifier(); + dnsMessageModifier.setMessageType(MessageType.QUERY); + dnsMessageModifier.setRecursionDesired(true); + dnsMessageModifier.setQuestionRecords(Collections.singletonList(questionRecord)); + currentMessage.add(dnsMessageModifier.getDnsMessage()); + } + + private Set getStoredRecords(QuestionRecord questionRecord) { + Set records = null; + try { + records = store.getRecords(questionRecord); + } catch (DnsException ignored) { + } + if (records != null) { + return records; + } + return new HashSet<>(); + } + + private void sendResponse(DohRecord record, HttpServerResponse response) { + response.putHeader("content-type", "application/dns-json"); + response.setStatusCode(200); + response.send(record.toJson().toBuffer()); + } + @Override public void stop() { - for (Transport transport : getTransports()) { - transport.getAcceptor().dispose(); + if (this.ssl) { + this.httpServer.close(); + } else { + for (Transport transport : getTransports()) { + transport.getAcceptor().dispose(); + } } } @@ -529,7 +622,7 @@ private void encode(DnsMessage dnsMessage, IoBuffer buf) { encoder.encode(buf, dnsMessage); - for (ResourceRecord record: dnsMessage.getAnswerRecords()) { + for (ResourceRecord record : dnsMessage.getAnswerRecords()) { // This is a hack to allow to also test for AAAA resolution as DnsMessageEncoder does not support it and it // is hard to extend, because the interesting methods are private... // In case of RecordType.AAAA we need to encode the RecordType by ourself @@ -554,10 +647,10 @@ public ProtocolEncoder getEncoder(IoSession session) throws Exception { @Override public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { - IoBuffer buf = IoBuffer.allocate( 1024 ); - FakeDNSServer.this.encode((DnsMessage)message, buf); + IoBuffer buf = IoBuffer.allocate(1024); + FakeDNSServer.this.encode((DnsMessage) message, buf); buf.flip(); - out.write( buf ); + out.write(buf); } }; } @@ -578,17 +671,17 @@ public ProtocolEncoder getEncoder(IoSession session) throws Exception { @Override public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { - IoBuffer buf = IoBuffer.allocate( 1024 ); - buf.putShort( ( short ) 0 ); + IoBuffer buf = IoBuffer.allocate(1024); + buf.putShort((short) 0); FakeDNSServer.this.encode((DnsMessage) message, buf); - encoder.encode( buf, ( DnsMessage ) message ); + encoder.encode(buf, (DnsMessage) message); int end = buf.position(); - short recordLength = ( short ) ( end - 2 ); + short recordLength = (short) (end - 2); buf.rewind(); - buf.putShort( recordLength ); - buf.position( end ); + buf.putShort(recordLength); + buf.position(end); buf.flip(); - out.write( buf ); + out.write(buf); } }; } diff --git a/src/test/java/io/vertx/test/tls/Trust.java b/src/test/java/io/vertx/test/tls/Trust.java index c10d59caca1..fb0ff863ef9 100644 --- a/src/test/java/io/vertx/test/tls/Trust.java +++ b/src/test/java/io/vertx/test/tls/Trust.java @@ -39,6 +39,7 @@ public interface Trust extends Supplier { Trust SNI_JKS_HOST3 = () -> new JksOptions().setPath("tls/sni-truststore-host3.jks").setPassword("wibble"); Trust SNI_JKS_HOST4 = () -> new JksOptions().setPath("tls/sni-truststore-host4.jks").setPassword("wibble"); Trust SNI_JKS_HOST5 = () -> new JksOptions().setPath("tls/sni-truststore-host5.jks").setPassword("wibble"); + Trust DOH_JKS_HOST = () -> new JksOptions().setPath("tls/server-keystore-doh.jks").setPassword("wibble"); Trust SNI_SERVER_ROOT_CA_AND_OTHER_CA_1 = () -> new JksOptions().setPath("tls/server-truststore-root-ca-host2.jks").setPassword("wibble"); Trust SNI_SERVER_ROOT_CA_AND_OTHER_CA_2 = () -> new JksOptions().setPath("tls/server-truststore-root-ca-host3.jks").setPassword("wibble"); Trust SNI_SERVER_ROOT_CA_FALLBACK = () -> new JksOptions().setPath("tls/server-truststore-root-ca-fallback.jks").setPassword("wibble"); diff --git a/src/test/resources/tls/server-keystore-doh.jks b/src/test/resources/tls/server-keystore-doh.jks new file mode 100644 index 00000000000..dd4212de469 Binary files /dev/null and b/src/test/resources/tls/server-keystore-doh.jks differ