From 054a5bc2e341bea2c3bb7b991339ec12771eb12b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 May 2024 14:59:51 +0200 Subject: [PATCH] get rid of ResolverApi and use DNS client directly --- build.gradle | 3 +- .../de/gultsch/minidns/AndroidDNSClient.java | 64 +- .../java/de/gultsch/minidns/DNSSocket.java | 36 +- .../de/gultsch/minidns/NetworkDataSource.java | 66 +- .../ResolutionUnsuccessfulException.java | 35 ++ .../de/gultsch/minidns/ResolverResult.java | 178 ++++++ .../eu/siacs/conversations/Conversations.java | 9 + .../services/XmppConnectionService.java | 1 - .../utils/AndroidUsingExecLowPriority.java | 92 --- .../utils/AndroidUsingLinkProperties.java | 90 --- .../siacs/conversations/utils/Resolver.java | 565 +++++++++--------- .../conversations/xmpp/XmppConnection.java | 2 +- 12 files changed, 590 insertions(+), 551 deletions(-) create mode 100644 src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java create mode 100644 src/main/java/de/gultsch/minidns/ResolverResult.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java diff --git a/build.gradle b/build.gradle index 700fd54e3ea60e97420a728fc9834f5d927469fe..9f1ae2334e49c2991576558a8e860f2fd4efd1fc 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,8 @@ dependencies { //zxing stopped supporting Java 7 so we have to stick with 3.3.3 //https://github.com/zxing/zxing/issues/1170 implementation 'com.google.zxing:core:3.3.3' - implementation 'de.measite.minidns:minidns-hla:0.2.4' + implementation 'org.minidns:minidns-client:1.0.4' + implementation 'org.minidns:minidns-dnssec:1.0.4' implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' diff --git a/src/main/java/de/gultsch/minidns/AndroidDNSClient.java b/src/main/java/de/gultsch/minidns/AndroidDNSClient.java index 2c3731871c1f7ab7cf2fb5eaa869867dc31fd93c..f1789f63a320eb8300903a82258f47ab1c26793d 100644 --- a/src/main/java/de/gultsch/minidns/AndroidDNSClient.java +++ b/src/main/java/de/gultsch/minidns/AndroidDNSClient.java @@ -14,10 +14,15 @@ import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -import de.measite.minidns.AbstractDNSClient; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.Record; -import de.measite.minidns.record.Data; +import org.minidns.AbstractDnsClient; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DirectCachedDnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsqueryresult.StandardDnsQueryResult; +import org.minidns.dnsqueryresult.SynthesizedCachedDnsQueryResult; +import org.minidns.record.Data; + +import org.minidns.record.Record; import eu.siacs.conversations.Config; @@ -27,11 +32,11 @@ import java.time.Duration; import java.util.Collections; import java.util.List; -public class AndroidDNSClient extends AbstractDNSClient { +public class AndroidDNSClient extends AbstractDnsClient { private static final long DNS_MAX_TTL = 86_400L; - private static final LruCache QUERY_CACHE = + private static final LruCache QUERY_CACHE = new LruCache<>(1024); private final Context context; private final NetworkDataSource networkDataSource = new NetworkDataSource(); @@ -60,7 +65,7 @@ public class AndroidDNSClient extends AbstractDNSClient { } @Override - protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) { + protected DnsMessage.Builder newQuestion(final DnsMessage.Builder message) { message.setRecursionDesired(true); message.getEdnsBuilder() .setUdpPayloadSize(networkDataSource.getUdpPayloadSize()) @@ -69,15 +74,16 @@ public class AndroidDNSClient extends AbstractDNSClient { } @Override - protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException { - final DNSMessage question = newQuestion(queryBuilder).build(); + protected DnsQueryResult query(final DnsMessage.Builder queryBuilder) throws IOException { + final DnsMessage question = newQuestion(queryBuilder).build(); for (final DNSServer dnsServer : getDNSServers()) { final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question); - final DNSMessage cachedResponse = queryCache(cacheKey); + final DnsMessage cachedResponse = queryCache(cacheKey); if (cachedResponse != null) { - return cachedResponse; + return new CachedDnsQueryResult(question, cachedResponse); } - final DNSMessage response = this.networkDataSource.query(question, dnsServer); + final DnsQueryResult result = this.networkDataSource.query(question, dnsServer); + final var response = result.response; if (response == null) { continue; } @@ -89,7 +95,7 @@ public class AndroidDNSClient extends AbstractDNSClient { continue; } cacheQuery(cacheKey, response); - return response; + return new StandardDnsQueryResult(dnsServer.inetAddress, dnsServer.port,result.queryMethod,question,response); } return null; } @@ -104,8 +110,7 @@ public class AndroidDNSClient extends AbstractDNSClient { private List getDNSServers() { final ImmutableList.Builder dnsServerBuilder = new ImmutableList.Builder<>(); - final ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final ConnectivityManager connectivityManager = context.getSystemService(ConnectivityManager.class); final Network[] networks = getActiveNetworks(connectivityManager); for (final Network network : networks) { final LinkProperties linkProperties = connectivityManager.getLinkProperties(network); @@ -133,17 +138,15 @@ public class AndroidDNSClient extends AbstractDNSClient { if (connectivityManager == null) { return new Network[0]; } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - final Network activeNetwork = connectivityManager.getActiveNetwork(); - if (activeNetwork != null) { + final Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork != null) { return new Network[] {activeNetwork}; - } } return connectivityManager.getAllNetworks(); } - private DNSMessage queryCache(final QuestionServerTuple key) { - final DNSMessage cachedResponse; + private DnsMessage queryCache(final QuestionServerTuple key) { + final DnsMessage cachedResponse; synchronized (QUERY_CACHE) { cachedResponse = QUERY_CACHE.get(key); if (cachedResponse == null) { @@ -163,7 +166,7 @@ public class AndroidDNSClient extends AbstractDNSClient { return cachedResponse; } - private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) { + private void cacheQuery(final QuestionServerTuple key, final DnsMessage response) { if (response.receiveTimestamp <= 0) { return; } @@ -172,7 +175,7 @@ public class AndroidDNSClient extends AbstractDNSClient { } } - private static long ttl(final DNSMessage dnsMessage) { + private static long ttl(final DnsMessage dnsMessage) { final List> answerSection = dnsMessage.answerSection; if (answerSection == null || answerSection.isEmpty()) { final List> authoritySection = dnsMessage.authoritySection; @@ -187,19 +190,19 @@ public class AndroidDNSClient extends AbstractDNSClient { } } - private static long expiresAt(final DNSMessage dnsMessage) { + private static long expiresAt(final DnsMessage dnsMessage) { return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L); } - private static long expiresIn(final DNSMessage dnsMessage) { + private static long expiresIn(final DnsMessage dnsMessage) { return expiresAt(dnsMessage) - System.currentTimeMillis(); } private static class QuestionServerTuple { private final DNSServer dnsServer; - private final DNSMessage question; + private final DnsMessage question; - private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) { + private QuestionServerTuple(final DNSServer dnsServer, final DnsMessage question) { this.dnsServer = dnsServer; this.question = question.asNormalizedVersion(); } @@ -218,4 +221,11 @@ public class AndroidDNSClient extends AbstractDNSClient { return Objects.hashCode(dnsServer, question); } } + + public static class CachedDnsQueryResult extends DnsQueryResult { + + private CachedDnsQueryResult(final DnsMessage query, final DnsMessage response) { + super(QueryMethod.cachedDirect, query, response); + } + } } diff --git a/src/main/java/de/gultsch/minidns/DNSSocket.java b/src/main/java/de/gultsch/minidns/DNSSocket.java index e3d86b80c33b24a12bca7dce025c46c144bd1432..4b7ad2bbd813cdeebcdbe908384c0e27b3d187d8 100644 --- a/src/main/java/de/gultsch/minidns/DNSSocket.java +++ b/src/main/java/de/gultsch/minidns/DNSSocket.java @@ -7,11 +7,11 @@ import com.google.common.base.Strings; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; -import de.measite.minidns.DNSMessage; import eu.siacs.conversations.Config; import org.conscrypt.OkHostnameVerifier; +import org.minidns.dnsmessage.DnsMessage; import java.io.Closeable; import java.io.DataInputStream; @@ -41,7 +41,7 @@ final class DNSSocket implements Closeable { public static final int QUERY_TIMEOUT = 5_000; private final Semaphore semaphore = new Semaphore(1); - private final Map> inFlightQueries = new HashMap<>(); + private final Map> inFlightQueries = new HashMap<>(); private final Socket socket; private final DataInputStream dataInputStream; private final DataOutputStream dataOutputStream; @@ -59,8 +59,8 @@ final class DNSSocket implements Closeable { private void readDNSMessages() { try { while (socket.isConnected()) { - final DNSMessage response = readDNSMessage(); - final SettableFuture future; + final DnsMessage response = readDNSMessage(); + final SettableFuture future; synchronized (inFlightQueries) { future = inFlightQueries.remove(response.id); } @@ -78,10 +78,10 @@ final class DNSSocket implements Closeable { private void evictInFlightQueries(final Exception e) { synchronized (inFlightQueries) { - final Iterator>> iterator = + final Iterator>> iterator = inFlightQueries.entrySet().iterator(); while (iterator.hasNext()) { - final Map.Entry> entry = iterator.next(); + final Map.Entry> entry = iterator.next(); entry.getValue().setException(e); iterator.remove(); } @@ -95,14 +95,11 @@ final class DNSSocket implements Closeable { } public static DNSSocket connect(final DNSServer dnsServer) throws IOException { - switch (dnsServer.uniqueTransport()) { - case TCP: - return connectTcpSocket(dnsServer); - case TLS: - return connectTlsSocket(dnsServer); - default: - throw new IllegalStateException("This is not a socket based transport"); - } + return switch (dnsServer.uniqueTransport()) { + case TCP -> connectTcpSocket(dnsServer); + case TLS -> connectTlsSocket(dnsServer); + default -> throw new IllegalStateException("This is not a socket based transport"); + }; } private static DNSSocket connectTcpSocket(final DNSServer dnsServer) throws IOException { @@ -133,10 +130,9 @@ final class DNSSocket implements Closeable { sslSocket.startHandshake(); final SSLSession session = sslSocket.getSession(); final Certificate[] peerCertificates = session.getPeerCertificates(); - if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate)) { + if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate certificate)) { throw new IOException("Peer did not provide X509 certificates"); } - final X509Certificate certificate = (X509Certificate) peerCertificates[0]; if (!OkHostnameVerifier.strictInstance().verify(dnsServer.hostname, certificate)) { throw new SSLPeerUnverifiedException("Peer did not provide valid certificates"); } @@ -144,7 +140,7 @@ final class DNSSocket implements Closeable { return DNSSocket.of(sslSocket); } - public DNSMessage query(final DNSMessage query) throws IOException, InterruptedException { + public DnsMessage query(final DnsMessage query) throws IOException, InterruptedException { try { return queryAsync(query).get(QUERY_TIMEOUT, TimeUnit.MILLISECONDS); } catch (final ExecutionException e) { @@ -159,9 +155,9 @@ final class DNSSocket implements Closeable { } } - public ListenableFuture queryAsync(final DNSMessage query) + public ListenableFuture queryAsync(final DnsMessage query) throws InterruptedException, IOException { - final SettableFuture responseFuture = SettableFuture.create(); + final SettableFuture responseFuture = SettableFuture.create(); synchronized (this.inFlightQueries) { this.inFlightQueries.put(query.id, responseFuture); } @@ -175,7 +171,7 @@ final class DNSSocket implements Closeable { return responseFuture; } - private DNSMessage readDNSMessage() throws IOException { + private DnsMessage readDNSMessage() throws IOException { final int length = this.dataInputStream.readUnsignedShort(); byte[] data = new byte[length]; int read = 0; diff --git a/src/main/java/de/gultsch/minidns/NetworkDataSource.java b/src/main/java/de/gultsch/minidns/NetworkDataSource.java index 67a8f8c334feb10a27749108b70ced563066bff8..0714594feaaa2c1afa4f8d757be8863c30a3d39e 100644 --- a/src/main/java/de/gultsch/minidns/NetworkDataSource.java +++ b/src/main/java/de/gultsch/minidns/NetworkDataSource.java @@ -11,13 +11,14 @@ import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.collect.ImmutableList; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.MiniDNSException; -import de.measite.minidns.source.DNSDataSource; -import de.measite.minidns.util.MultipleIoException; - import eu.siacs.conversations.Config; +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsqueryresult.StandardDnsQueryResult; +import org.minidns.util.MultipleIoException; + import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; @@ -28,7 +29,7 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -public class NetworkDataSource extends DNSDataSource { +public class NetworkDataSource extends org.minidns.source.NetworkDataSource { private static final LoadingCache socketCache = CacheBuilder.newBuilder() @@ -66,7 +67,8 @@ public class NetworkDataSource extends DNSDataSource { } @Override - public DNSMessage query(final DNSMessage message, final InetAddress address, final int port) + public StandardDnsQueryResult query( + final DnsMessage message, final InetAddress address, final int port) throws IOException { final List transports = transportsForPort(port); Log.w( @@ -80,16 +82,21 @@ public class NetworkDataSource extends DNSDataSource { return query(message, new DNSServer(address, port, transports)); } - public DNSMessage query(final DNSMessage message, final DNSServer dnsServer) + public StandardDnsQueryResult query(final DnsMessage message, final DNSServer dnsServer) throws IOException { Log.d(Config.LOGTAG, "using " + dnsServer); final List ioExceptions = new ArrayList<>(); for (final Transport transport : dnsServer.transports) { try { - final DNSMessage response = + final DnsMessage response = queryWithUniqueTransport(message, dnsServer.asUniqueTransport(transport)); if (response != null && !response.truncated) { - return response; + return new StandardDnsQueryResult( + dnsServer.inetAddress, + dnsServer.port, + transportToMethod(transport), + message, + response); } } catch (final IOException e) { ioExceptions.add(e); @@ -101,23 +108,26 @@ public class NetworkDataSource extends DNSDataSource { return null; } - private DNSMessage queryWithUniqueTransport(final DNSMessage message, final DNSServer dnsServer) + private static DnsQueryResult.QueryMethod transportToMethod(final Transport transport) { + return switch (transport) { + case UDP -> DnsQueryResult.QueryMethod.udp; + default -> DnsQueryResult.QueryMethod.tcp; + }; + } + + private DnsMessage queryWithUniqueTransport(final DnsMessage message, final DNSServer dnsServer) throws IOException, InterruptedException { final Transport transport = dnsServer.uniqueTransport(); - switch (transport) { - case UDP: - return queryUdp(message, dnsServer.inetAddress, dnsServer.port); - case TCP: - case TLS: - return queryDnsSocket(message, dnsServer); - default: - throw new IOException( - String.format("Transport %s has not been implemented", transport)); - } + return switch (transport) { + case UDP -> queryUdp(message, dnsServer.inetAddress, dnsServer.port); + case TCP, TLS -> queryDnsSocket(message, dnsServer); + default -> throw new IOException( + String.format("Transport %s has not been implemented", transport)); + }; } - protected DNSMessage queryUdp( - final DNSMessage message, final InetAddress address, final int port) + protected DnsMessage queryUdp( + final DnsMessage message, final InetAddress address, final int port) throws IOException { final DatagramPacket request = message.asDatagram(address, port); final byte[] buffer = new byte[udpPayloadSize]; @@ -126,15 +136,15 @@ public class NetworkDataSource extends DNSDataSource { socket.send(request); final DatagramPacket response = new DatagramPacket(buffer, buffer.length); socket.receive(response); - final DNSMessage dnsMessage = readDNSMessage(response.getData()); + final DnsMessage dnsMessage = readDNSMessage(response.getData()); if (dnsMessage.id != message.id) { - throw new MiniDNSException.IdMismatch(message, dnsMessage); + throw new MiniDnsException.IdMismatch(message, dnsMessage); } return dnsMessage; } } - protected DNSMessage queryDnsSocket(final DNSMessage message, final DNSServer dnsServer) + protected DnsMessage queryDnsSocket(final DnsMessage message, final DNSServer dnsServer) throws IOException, InterruptedException { final DNSSocket cachedDnsSocket = socketCache.getIfPresent(dnsServer); if (cachedDnsSocket != null) { @@ -159,9 +169,9 @@ public class NetworkDataSource extends DNSDataSource { } } - public static DNSMessage readDNSMessage(final byte[] bytes) throws IOException { + public static DnsMessage readDNSMessage(final byte[] bytes) throws IOException { try { - return new DNSMessage(bytes); + return new DnsMessage(bytes); } catch (final IllegalArgumentException e) { throw new IOException(Throwables.getRootCause(e)); } diff --git a/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java new file mode 100644 index 0000000000000000000000000000000000000000..e5e406ca7a6ff6a947f99b0a7a99101c900c6b71 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; + +import java.io.Serial; + +public class ResolutionUnsuccessfulException extends MiniDnsException { + + /** + * + */ + @Serial + private static final long serialVersionUID = 1L; + + public final Question question; + public final RESPONSE_CODE responseCode; + + public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) { + super("Asking for " + question + " yielded an error response " + responseCode); + this.question = question; + this.responseCode = responseCode; + } +} diff --git a/src/main/java/de/gultsch/minidns/ResolverResult.java b/src/main/java/de/gultsch/minidns/ResolverResult.java new file mode 100644 index 0000000000000000000000000000000000000000..3d80fb8df4d3dbffa43fa2924c2991497e2a8483 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolverResult.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnssec.DnssecResultNotAuthenticException; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; + +public class ResolverResult { + + protected final Question question; + private final RESPONSE_CODE responseCode; + private final Set data; + private final boolean isAuthenticData; + protected final Set unverifiedReasons; + protected final DnsMessage answer; + protected final DnsQueryResult result; + + public ResolverResult(Question question, DnsQueryResult result, Set unverifiedReasons) throws NullResultException { + // TODO: Is this null check still needed? + if (result == null) { + throw new MiniDnsException.NullResultException(question.asMessageBuilder().build()); + } + + this.result = result; + + DnsMessage answer = result.response; + this.question = question; + this.responseCode = answer.responseCode; + this.answer = answer; + + Set r = answer.getAnswersFor(question); + if (r == null) { + this.data = Collections.emptySet(); + } else { + this.data = Collections.unmodifiableSet(r); + } + + if (unverifiedReasons == null) { + this.unverifiedReasons = null; + isAuthenticData = false; + } else { + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + isAuthenticData = this.unverifiedReasons.isEmpty(); + } + } + + public boolean wasSuccessful() { + return responseCode == RESPONSE_CODE.NO_ERROR; + } + + public Set getAnswers() { + throwIseIfErrorResponse(); + return data; + } + + public Set getAnswersOrEmptySet() { + return data; + } + + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + public boolean isAuthenticData() { + throwIseIfErrorResponse(); + return isAuthenticData; + } + + /** + * Get the reasons the result could not be verified if any exists. + * + * @return The reasons the result could not be verified or null. + */ + public Set getUnverifiedReasons() { + throwIseIfErrorResponse(); + return unverifiedReasons; + } + + public Question getQuestion() { + return question; + } + + public void throwIfErrorResponse() throws ResolutionUnsuccessfulException { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException; + } + + private ResolutionUnsuccessfulException resolutionUnsuccessfulException; + + public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() { + if (wasSuccessful()) return null; + + if (resolutionUnsuccessfulException == null) { + resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + } + + return resolutionUnsuccessfulException; + } + + private DnssecResultNotAuthenticException dnssecResultNotAuthenticException; + + public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() { + if (!wasSuccessful()) + return null; + if (isAuthenticData) + return null; + + if (dnssecResultNotAuthenticException == null) { + dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons()); + } + + return dnssecResultNotAuthenticException; + } + + /** + * Get the raw answer DNS message we received. This is likely not what you want, try {@link #getAnswers()} instead. + * + * @return the raw answer DNS Message. + * @see #getAnswers() + */ + public DnsMessage getRawAnswer() { + return answer; + } + + public DnsQueryResult getDnsQueryResult() { + return result; + } + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(getClass().getName()).append('\n') + .append("Question: ").append(question).append('\n') + .append("Response Code: ").append(responseCode).append('\n'); + + if (responseCode == RESPONSE_CODE.NO_ERROR) { + if (isAuthenticData) { + sb.append("Results verified via DNSSEC\n"); + } + if (hasUnverifiedReasons()) { + sb.append(unverifiedReasons).append('\n'); + } + sb.append(answer.answerSection); + } + + return sb.toString(); + } + + boolean hasUnverifiedReasons() { + return unverifiedReasons != null && !unverifiedReasons.isEmpty(); + } + + protected void throwIseIfErrorResponse() { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) + throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful", + resolutionUnsuccessfulException); + } +} diff --git a/src/main/java/eu/siacs/conversations/Conversations.java b/src/main/java/eu/siacs/conversations/Conversations.java index 8b564c0cfa2d04e5ee0a300ae44a55b0478efc24..72ed0a72b9ec618d8c779ec9485dcdc2567e0843 100644 --- a/src/main/java/eu/siacs/conversations/Conversations.java +++ b/src/main/java/eu/siacs/conversations/Conversations.java @@ -1,5 +1,6 @@ package eu.siacs.conversations; +import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; @@ -14,9 +15,17 @@ import eu.siacs.conversations.utils.ExceptionHelper; public class Conversations extends Application { + @SuppressLint("StaticFieldLeak") + private static Context CONTEXT; + + public static Context getContext() { + return Conversations.CONTEXT; + } + @Override public void onCreate() { super.onCreate(); + CONTEXT = this.getApplicationContext(); ExceptionHelper.init(getApplicationContext()); applyThemeSettings(); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 89efa949a3185ec722b4def9abccfe7a97b2e5e6..484eaea0daafb15102b2c5503b7a666c5b47268b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1267,7 +1267,6 @@ public class XmppConnectionService extends Service { } catch (Throwable throwable) { Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } - Resolver.init(this); updateMemorizingTrustManager(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; diff --git a/src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java b/src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java deleted file mode 100644 index d8c46b08f7ae2b1b3b71bd2abd0a58752fb58719..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015-2016 the original author or authors - * - * This software is licensed under the Apache License, Version 2.0, - * the GNU Lesser General Public License version 2 or later ("LGPL") - * and the WTFPL. - * You may choose either license to govern your use of this software only - * upon the condition that you accept all of the terms of either - * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. - */ -package eu.siacs.conversations.utils; - - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.LineNumberReader; -import java.net.InetAddress; -import java.util.HashSet; -import java.util.logging.Level; - -import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; -import de.measite.minidns.dnsserverlookup.AndroidUsingReflection; -import de.measite.minidns.dnsserverlookup.DNSServerLookupMechanism; -import de.measite.minidns.util.PlatformDetection; - -/** - * Try to retrieve the list of DNS server by executing getprop. - */ -public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanism { - - public static final DNSServerLookupMechanism INSTANCE = new AndroidUsingExecLowPriority(); - public static final int PRIORITY = AndroidUsingReflection.PRIORITY + 1; - - private AndroidUsingExecLowPriority() { - super(AndroidUsingExecLowPriority.class.getSimpleName(), PRIORITY); - } - - @Override - public String[] getDnsServerAddresses() { - try { - Process process = Runtime.getRuntime().exec("getprop"); - InputStream inputStream = process.getInputStream(); - LineNumberReader lnr = new LineNumberReader( - new InputStreamReader(inputStream)); - String line; - HashSet server = new HashSet<>(6); - while ((line = lnr.readLine()) != null) { - int split = line.indexOf("]: ["); - if (split == -1) { - continue; - } - String property = line.substring(1, split); - String value = line.substring(split + 4, line.length() - 1); - - if (value.isEmpty()) { - continue; - } - - if (property.endsWith(".dns") || property.endsWith(".dns1") || - property.endsWith(".dns2") || property.endsWith(".dns3") || - property.endsWith(".dns4")) { - - // normalize the address - - InetAddress ip = InetAddress.getByName(value); - - if (ip == null) continue; - - value = ip.getHostAddress(); - - if (value == null) continue; - if (value.length() == 0) continue; - - server.add(value); - } - } - if (server.size() > 0) { - return server.toArray(new String[server.size()]); - } - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e); - } - return null; - } - - @Override - public boolean isAvailable() { - return PlatformDetection.isAndroid(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java b/src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java deleted file mode 100644 index 658e7abcd7e8d1046fc51b02a3944e48aacf265f..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java +++ /dev/null @@ -1,90 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.annotation.TargetApi; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.Network; -import android.net.NetworkInfo; -import android.net.RouteInfo; -import android.os.Build; - -import java.net.Inet4Address; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.List; - -import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; -import de.measite.minidns.dnsserverlookup.AndroidUsingExec; - -public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism { - - private final Context context; - - AndroidUsingLinkProperties(Context context) { - super(AndroidUsingLinkProperties.class.getSimpleName(), AndroidUsingExec.PRIORITY - 1); - this.context = context; - } - - @Override - public boolean isAvailable() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - - @Override - @TargetApi(21) - public String[] getDnsServerAddresses() { - final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - final Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks(); - if (networks == null) { - return new String[0]; - } - final Network activeNetwork = getActiveNetwork(connectivityManager); - final List servers = new ArrayList<>(); - int vpnOffset = 0; - for(Network network : networks) { - LinkProperties linkProperties = connectivityManager.getLinkProperties(network); - if (linkProperties == null) { - continue; - } - final NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network); - final boolean isActiveNetwork = network.equals(activeNetwork); - final boolean isVpn = networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_VPN; - if (isActiveNetwork && isVpn) { - final List tmp = getIPv4First(linkProperties.getDnsServers()); - servers.addAll(0, tmp); - vpnOffset += tmp.size(); - } else if (hasDefaultRoute(linkProperties) || isActiveNetwork || activeNetwork == null || isVpn) { - servers.addAll(vpnOffset, getIPv4First(linkProperties.getDnsServers())); - } - } - return servers.toArray(new String[0]); - } - - @TargetApi(23) - private static Network getActiveNetwork(ConnectivityManager cm) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? cm.getActiveNetwork() : null; - } - - private static List getIPv4First(List in) { - List out = new ArrayList<>(); - for(InetAddress address : in) { - if (address instanceof Inet4Address) { - out.add(0, address.getHostAddress()); - } else { - out.add(address.getHostAddress()); - } - } - return out; - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static boolean hasDefaultRoute(LinkProperties linkProperties) { - for(RouteInfo route: linkProperties.getRoutes()) { - if (route.isDefaultRoute()) { - return true; - } - } - return false; - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 6746f3ea9a5d8216f4c966faab907ca3acbd43fe..88f2c601e47ad4812e65afd80146102554d8ce57 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -6,85 +6,83 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Strings; -import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.common.net.InetAddresses; import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import de.gultsch.minidns.AndroidDNSClient; +import de.gultsch.minidns.ResolverResult; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.Conversations; +import eu.siacs.conversations.xmpp.Jid; + +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.CNAME; +import org.minidns.record.Data; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.Record; +import org.minidns.record.SRV; -import java.io.IOException; -import java.lang.reflect.Field; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; - -import de.gultsch.minidns.AndroidDNSClient; -import de.measite.minidns.AbstractDNSClient; -import de.measite.minidns.DNSCache; -import de.measite.minidns.DNSClient; -import de.measite.minidns.DNSName; -import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.cache.LRUCache; -import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException; -import de.measite.minidns.dnsserverlookup.AndroidUsingExec; -import de.measite.minidns.hla.DnssecResolverApi; -import de.measite.minidns.hla.ResolverApi; -import de.measite.minidns.hla.ResolverResult; -import de.measite.minidns.iterative.ReliableDNSClient; -import de.measite.minidns.record.A; -import de.measite.minidns.record.AAAA; -import de.measite.minidns.record.CNAME; -import de.measite.minidns.record.Data; -import de.measite.minidns.record.InternetAddressRR; -import de.measite.minidns.record.SRV; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class Resolver { + private static final Comparator RESULT_COMPARATOR = + (left, right) -> { + if (left.priority == right.priority) { + if (left.directTls == right.directTls) { + if (left.ip == null && right.ip == null) { + return 0; + } else if (left.ip != null && right.ip != null) { + if (left.ip instanceof Inet4Address + && right.ip instanceof Inet4Address) { + return 0; + } else { + return left.ip instanceof Inet4Address ? -1 : 1; + } + } else { + return left.ip != null ? -1 : 1; + } + } else { + return left.directTls ? -1 : 1; + } + } else { + return left.priority - right.priority; + } + }; + + private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12); + public static final int DEFAULT_PORT_XMPP = 5222; private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; private static final String STARTTLS_SERVICE = "_xmpp-client"; - private static XmppConnectionService SERVICE = null; - - - public static void init(XmppConnectionService service) { - Resolver.SERVICE = service; - DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE); - DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE); - DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service)); - final AbstractDNSClient client = ResolverApi.INSTANCE.getClient(); - if (client instanceof ReliableDNSClient) { - disableHardcodedDnsServers((ReliableDNSClient) client); - } - } - - private static void disableHardcodedDnsServers(ReliableDNSClient reliableDNSClient) { - try { - final Field dnsClientField = ReliableDNSClient.class.getDeclaredField("dnsClient"); - dnsClientField.setAccessible(true); - final DNSClient dnsClient = (DNSClient) dnsClientField.get(reliableDNSClient); - if (dnsClient != null) { - dnsClient.getDataSource().setTimeout(3000); - } - final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); - useHardcodedDnsServers.setAccessible(true); - useHardcodedDnsServers.setBoolean(dnsClient, false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); - } - } - public static List fromHardCoded(final String hostname, final int port) { final Result result = new Result(); - result.hostname = DNSName.from(hostname); + result.hostname = DnsName.from(hostname); result.port = port; result.directTls = useDirectTls(port); result.authenticated = true; @@ -92,221 +90,236 @@ public class Resolver { } public static void checkDomain(final Jid jid) { - DNSName.from(jid.getDomain()); + DnsName.from(jid.getDomain()); } public static boolean invalidHostname(final String hostname) { try { - DNSName.from(hostname); + DnsName.from(hostname); return false; } catch (IllegalArgumentException e) { return true; } } - public static void clearCache() { - final AbstractDNSClient client = ResolverApi.INSTANCE.getClient(); - final DNSCache dnsCache = client.getCache(); - if (dnsCache instanceof LRUCache) { - Log.d(Config.LOGTAG,"clearing DNS cache"); - ((LRUCache) dnsCache).clear(); - } - } - + public static void clearCache() {} public static boolean useDirectTls(final int port) { return port == 443 || port == 5223; } public static List resolve(final String domain) { - final List ipResults = fromIpAddress(domain); - if (ipResults.size() > 0) { + final List ipResults = fromIpAddress(domain); + if (!ipResults.isEmpty()) { return ipResults; } - final List results = new ArrayList<>(); - final List fallbackResults = new ArrayList<>(); - final Thread[] threads = new Thread[3]; - threads[0] = new Thread(() -> { - try { - final List list = resolveSrv(domain, true); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); - } - } - }); - threads[1] = new Thread(() -> { - try { - final List list = resolveSrv(domain, false); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); - } - } - }); - threads[2] = new Thread(() -> { - List list = resolveNoSrvRecords(DNSName.from(domain), true); - synchronized (fallbackResults) { - fallbackResults.addAll(list); - } - }); - for (final Thread thread : threads) { - thread.start(); - } + + final var startTls = resolveSrvAsFuture(domain, false); + final var directTls = resolveSrvAsFuture(domain, true); + + final var combined = merge(ImmutableList.of(startTls, directTls)); + + final var combinedWithFallback = + Futures.transformAsync( + combined, + results -> { + if (results.isEmpty()) { + return resolveNoSrvAsFuture(DnsName.from(domain), true); + } else { + return Futures.immediateFuture(results); + } + }, + MoreExecutors.directExecutor()); + final var orderedFuture = + Futures.transform( + combinedWithFallback, + all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all), + MoreExecutors.directExecutor()); try { - threads[0].join(); - threads[1].join(); - if (results.size() > 0) { - threads[2].interrupt(); - synchronized (results) { - Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results); - return results; - } - } else { - threads[2].join(); - synchronized (fallbackResults) { - Collections.sort(fallbackResults); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults); - return fallbackResults; - } - } - } catch (InterruptedException e) { - for (Thread thread : threads) { - thread.interrupt(); - } + final var ordered = orderedFuture.get(); + Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered); + return ordered; + } catch (final ExecutionException e) { + Log.d(Config.LOGTAG, "error resolving DNS", e); + return Collections.emptyList(); + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "DNS resolution interrupted"); return Collections.emptyList(); } } - private static List fromIpAddress(String domain) { - if (!IP.matches(domain)) { - return Collections.emptyList(); - } - try { - Result result = new Result(); - result.ip = InetAddress.getByName(domain); + private static List fromIpAddress(final String domain) { + if (IP.matches(domain)) { + final InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(domain); + } catch (final UnknownHostException e) { + return Collections.emptyList(); + } + final Result result = new Result(); + result.ip = inetAddress; result.port = DEFAULT_PORT_XMPP; return Collections.singletonList(result); - } catch (UnknownHostException e) { + } else { return Collections.emptyList(); } } - private static List resolveSrv(String domain, final boolean directTls) throws IOException { - DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); - ResolverResult result = resolveWithFallback(dnsName, SRV.class); - final List results = new ArrayList<>(); - final List threads = new ArrayList<>(); - for (SRV record : result.getAnswersOrEmptySet()) { - if (record.name.length() == 0 && record.priority == 0) { - continue; - } - threads.add(new Thread(() -> { - final List ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls); - if (ipv4s.size() == 0) { - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = result.isAuthenticData(); - ipv4s.add(resolverResult); - } - synchronized (results) { - results.addAll(ipv4s); - } - - })); - threads.add(new Thread(() -> { - final List ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls); - synchronized (results) { - results.addAll(ipv6s); - } - })); - } - for (Thread thread : threads) { - thread.start(); - } - for (Thread thread : threads) { - try { - thread.join(); - } catch (InterruptedException e) { - return Collections.emptyList(); - } - } - return results; + private static ListenableFuture> resolveSrvAsFuture( + final String domain, final boolean directTls) { + final DnsName dnsName = + DnsName.from( + (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); + final var resultFuture = resolveAsFuture(dnsName, SRV.class); + return Futures.transformAsync( + resultFuture, + result -> resolveIpsAsFuture(result, directTls), + MoreExecutors.directExecutor()); } - private static List resolveIp(SRV srv, Class type, boolean authenticated, boolean directTls) { - List list = new ArrayList<>(); - try { - ResolverResult results = resolveWithFallback(srv.name, type, authenticated); - for (D record : results.getAnswersOrEmptySet()) { - Result resolverResult = Result.fromRecord(srv, directTls); - resolverResult.authenticated = results.isAuthenticData() && authenticated; //TODO technically it doesn’t matter if the IP was authenticated - resolverResult.ip = record.getInetAddress(); - list.add(resolverResult); + @NonNull + private static ListenableFuture> resolveIpsAsFuture( + final ResolverResult srvResolverResult, final boolean directTls) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) { + if (record.target.length() == 0 && record.priority == 0) { + continue; } - } catch (Throwable t) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage()); + final var ipv4sRaw = + resolveIpsAsFuture( + record, A.class, srvResolverResult.isAuthenticData(), directTls); + final var ipv4s = + Futures.transform( + ipv4sRaw, + results -> { + if (results.isEmpty()) { + final Result resolverResult = + Result.fromRecord(record, directTls); + resolverResult.authenticated = + srvResolverResult.isAuthenticData(); + return Collections.singletonList(resolverResult); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); + final var ipv6s = + resolveIpsAsFuture( + record, AAAA.class, srvResolverResult.isAuthenticData(), directTls); + futuresBuilder.add(ipv4s); + futuresBuilder.add(ipv6s); } - return list; + final ImmutableList>> futures = futuresBuilder.build(); + return merge(futures); } - private static List resolveNoSrvRecords(DNSName dnsName, boolean withCnames) { - final List results = new ArrayList<>(); - try { - for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) { - results.add(Result.createDefault(dnsName, a.getInetAddress())); - } - for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) { - results.add(Result.createDefault(dnsName, aaaa.getInetAddress())); - } - if (results.size() == 0 && withCnames) { - for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) { - results.addAll(resolveNoSrvRecords(cname.name, false)); - } - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); - } - } - results.add(Result.createDefault(dnsName)); - return results; + private static ListenableFuture> merge( + final Collection>> futures) { + return Futures.transform( + Futures.successfulAsList(futures), + lists -> { + final var builder = new ImmutableList.Builder(); + for (final Collection list : lists) { + if (list == null) { + continue; + } + builder.addAll(list); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); } - private static ResolverResult resolveWithFallback(DNSName dnsName, Class type) throws IOException { - return resolveWithFallback(dnsName, type, validateHostname()); + private static > + ListenableFuture> resolveIpsAsFuture( + final SRV srv, Class type, boolean authenticated, boolean directTls) { + final var resultFuture = resolveAsFuture(srv.target, type); + return Futures.transform( + resultFuture, + result -> { + final var builder = new ImmutableList.Builder(); + for (D record : result.getAnswersOrEmptySet()) { + Result resolverResult = Result.fromRecord(srv, directTls); + resolverResult.authenticated = + result.isAuthenticData() + && authenticated; // TODO technically it does not matter if + // the IP + // was authenticated + resolverResult.ip = record.getInetAddress(); + builder.add(resolverResult); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); } - private static ResolverResult resolveWithFallback(DNSName dnsName, Class type, boolean validateHostname) throws IOException { - final Question question = new Question(dnsName, Record.TYPE.getType(type)); - if (!validateHostname) { - final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE); - final ResolverApi resolverApi = new ResolverApi(androidDNSClient); - return resolverApi.resolve(question); - } - try { - return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question); - } catch (DNSSECResultNotAuthenticException e) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); - } catch (IOException e) { - throw e; - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); + private static ListenableFuture> resolveNoSrvAsFuture( + final DnsName dnsName, boolean cName) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + ListenableFuture> aRecordResults = + Futures.transform( + resolveAsFuture(dnsName, A.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + a -> Result.createDefault(dnsName, a.getInetAddress())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aRecordResults); + ListenableFuture> aaaaRecordResults = + Futures.transform( + resolveAsFuture(dnsName, AAAA.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + aaaa -> + Result.createDefault( + dnsName, aaaa.getInetAddress())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aaaaRecordResults); + if (cName) { + ListenableFuture> cNameRecordResults = + Futures.transformAsync( + resolveAsFuture(dnsName, CNAME.class), + result -> { + Collection>> test = + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + cname -> resolveNoSrvAsFuture(cname.target, false)); + return merge(test); + }, + MoreExecutors.directExecutor()); + futuresBuilder.add(cNameRecordResults); } - return ResolverApi.INSTANCE.resolve(question); + final ImmutableList>> futures = futuresBuilder.build(); + final var noSrvFallbacks = merge(futures); + return Futures.transform( + noSrvFallbacks, + results -> { + if (results.isEmpty()) { + return Collections.singletonList(Result.createDefault(dnsName)); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); } - private static boolean validateHostname() { - return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname); + private static ListenableFuture> resolveAsFuture( + final DnsName dnsName, final Class type) { + return Futures.submit( + () -> { + final Question question = new Question(dnsName, Record.TYPE.getType(type)); + final AndroidDNSClient androidDNSClient = + new AndroidDNSClient(Conversations.getContext()); + final DnsQueryResult dnsQueryResult = androidDNSClient.query(question); + return new ResolverResult(question, dnsQueryResult, null); + }, + DNS_QUERY_EXECUTOR); } - public static class Result implements Comparable { + public static class Result { public static final String DOMAIN = "domain"; public static final String IP = "ip"; public static final String HOSTNAME = "hostname"; @@ -315,7 +328,7 @@ public class Resolver { public static final String DIRECT_TLS = "directTls"; public static final String AUTHENTICATED = "authenticated"; private InetAddress ip; - private DNSName hostname; + private DnsName hostname; private int port = DEFAULT_PORT_XMPP; private boolean directTls = false; private boolean authenticated = false; @@ -330,7 +343,7 @@ public class Resolver { return result; } - static Result createDefault(DNSName hostname, InetAddress ip) { + static Result createDefault(DnsName hostname, InetAddress ip) { Result result = new Result(); result.port = DEFAULT_PORT_XMPP; result.hostname = hostname; @@ -338,23 +351,24 @@ public class Resolver { return result; } - static Result createDefault(DNSName hostname) { + static Result createDefault(DnsName hostname) { return createDefault(hostname, null); } public static Result fromCursor(Cursor cursor) { final Result result = new Result(); try { - result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP))); + result.ip = + InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP))); } catch (UnknownHostException e) { result.ip = null; } - final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME)); - result.hostname = hostname == null ? null : DNSName.from(hostname); - result.port = cursor.getInt(cursor.getColumnIndex(PORT)); - result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); - result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; - result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; + final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)); + result.hostname = hostname == null ? null : DnsName.from(hostname); + result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT)); + result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY)); + result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0; + result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0; return result; } @@ -362,26 +376,18 @@ public class Resolver { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Result result = (Result) o; - - if (port != result.port) return false; - if (directTls != result.directTls) return false; - if (authenticated != result.authenticated) return false; - if (priority != result.priority) return false; - if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false; - return hostname != null ? hostname.equals(result.hostname) : result.hostname == null; + return port == result.port + && directTls == result.directTls + && authenticated == result.authenticated + && priority == result.priority + && Objects.equal(ip, result.ip) + && Objects.equal(hostname, result.hostname); } @Override public int hashCode() { - int result = ip != null ? ip.hashCode() : 0; - result = 31 * result + (hostname != null ? hostname.hashCode() : 0); - result = 31 * result + port; - result = 31 * result + (directTls ? 1 : 0); - result = 31 * result + (authenticated ? 1 : 0); - result = 31 * result + priority; - return result; + return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority); } public InetAddress getIp() { @@ -392,7 +398,7 @@ public class Resolver { return port; } - public DNSName getHostname() { + public DnsName getHostname() { return hostname; } @@ -405,38 +411,16 @@ public class Resolver { } @Override + @NonNull public String toString() { - return "Result{" + - "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' + - ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' + - ", port=" + port + - ", directTls=" + directTls + - ", authenticated=" + authenticated + - ", priority=" + priority + - '}'; - } - - @Override - public int compareTo(@NonNull Result result) { - if (result.priority == priority) { - if (directTls == result.directTls) { - if (ip == null && result.ip == null) { - return 0; - } else if (ip != null && result.ip != null) { - if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) { - return 0; - } else { - return ip instanceof Inet4Address ? -1 : 1; - } - } else { - return ip != null ? -1 : 1; - } - } else { - return directTls ? -1 : 1; - } - } else { - return priority - result.priority; - } + return MoreObjects.toStringHelper(this) + .add("ip", ip) + .add("hostname", hostname) + .add("port", port) + .add("directTls", directTls) + .add("authenticated", authenticated) + .add("priority", priority) + .toString(); } public ContentValues toContentValues() { @@ -482,7 +466,7 @@ public class Resolver { return null; } try { - result.hostname = DNSName.from(hostPart.trim()); + result.hostname = DnsName.from(hostPart.trim()); } catch (final Exception e) { return null; } @@ -499,7 +483,7 @@ public class Resolver { result.ip = inetAddress; } else { try { - result.hostname = DNSName.from(hostname); + result.hostname = DnsName.from(hostname); } catch (final Exception e) { return null; } @@ -509,5 +493,4 @@ public class Resolver { return result; } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6390e891cb79f09f2325ea9e0c1ba5b87e67a3ae..31e150ac7447734b48112377aad6bf50aa927a09 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -354,7 +354,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); return; } - if (results.size() == 0) { + if (results.isEmpty()) { Log.e( Config.LOGTAG, account.getJid().asBareJid() + ": Resolver results were empty");