diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 0b913bd39..ed805e03d 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -7,8 +7,17 @@ import android.util.Log; import java.io.IOException; import java.lang.reflect.Field; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.List; import de.measite.minidns.AbstractDNSClient; @@ -21,8 +30,11 @@ 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; @@ -62,13 +74,13 @@ public class Resolver { } } - public static List fromHardCoded(String hostname, int port) { - Result result = new Result(); - result.hostname = DNSName.from(hostname); - result.port = port; - result.directTls = useDirectTls(port); - result.authenticated = true; - return Collections.singletonList(result); + public static Result fromHardCoded(String hostname, int port) { + final Result ipResult = fromIpAddress(hostname, port); + if (ipResult != null) { + ipResult.call(); + return ipResult; + } + return happyEyeball(resolveNoSrvRecords(DNSName.from(hostname), true)); } @@ -76,10 +88,11 @@ public class Resolver { return port == 443 || port == 5223; } - public static List resolve(String domain) { - final List ipResults = fromIpAddress(domain); - if (ipResults.size() > 0) { - return ipResults; + public static Result resolve(String domain) { + final Result ipResult = fromIpAddress(domain); + if (ipResult != null) { + ipResult.call(); + return ipResult; } final List results = new ArrayList<>(); final List fallbackResults = new ArrayList<>(); @@ -121,63 +134,109 @@ public class Resolver { synchronized (results) { Collections.sort(results); Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString()); - return new ArrayList<>(results); + return happyEyeball(results); } } else { threads[2].join(); synchronized (fallbackResults) { Collections.sort(fallbackResults); Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString()); - return new ArrayList<>(fallbackResults); + return happyEyeball(fallbackResults); } } } catch (InterruptedException e) { for (Thread thread : threads) { thread.interrupt(); } - return Collections.emptyList(); + return null; } } - private static List fromIpAddress(String domain) { - if (!IP.matches(domain)) { - return Collections.emptyList(); + private static Result fromIpAddress(String domain) { + return fromIpAddress(domain, DEFAULT_PORT_XMPP); + } + private static Result fromIpAddress(String domain, int port) { + if (IP.matches(domain)) { + Result result = new Result(InetAddress.getByName(domain), port); + result.authenticated = true; + return result; } - return Collections.singletonList(Result.createDefault(DNSName.from(domain))); + return null; } 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; } - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = result.isAuthenticData(); - results.add(resolverResult); - } + threads.add(new Thread(() -> { + final List ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls); + 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 List resolveNoSrvRecords(DNSName dnsName, boolean withCnames) { - List results = new ArrayList<>(); - Boolean resolveCNAME = false; + private static List resolveIp(SRV srv, Class type, boolean authenticated, boolean directTls) { + List list = new ArrayList<>(); try { - if (withCnames) { + ResolverResult results = resolveWithFallback(srv.name, type, authenticated); + for (D record : results.getAnswersOrEmptySet()) { + Result resolverResult = new Result(srv.name, record.getInetAddress(), srv.port, srv.priority); + resolverResult.authenticated = results.isAuthenticData() && authenticated; + resolverResult.directTls = directTls; + list.add(resolverResult); + } + } catch (Throwable t) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage()); + } + return list; + } + + private static List resolveNoSrvRecords(DNSName dnsName, boolean withCnames) { + return resolveNoSrvRecords(dnsName, DEFAULT_PORT_XMPP, withCnames); + } + + private static List resolveNoSrvRecords(DNSName dnsName, int port, boolean withCnames) { + List results = new ArrayList<>(); + try { + for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) { + results.add(new Result(dnsName, a.getInetAddress(), port)); + } + for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) { + results.add(new Result(dnsName, aaaa.getInetAddress(), port)); + } + if (results.size() == 0 && withCnames) { for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) { - results.addAll(resolveNoSrvRecords(cname.name, false)); - resolveCNAME = true; + results.addAll(resolveNoSrvRecords(cname.name, port, false)); } } } catch (Throwable throwable) { Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); } - if(!resolveCNAME) { - results.add(Result.createDefault(dnsName)); - } return results; } @@ -202,48 +261,57 @@ public class Resolver { return ResolverApi.INSTANCE.resolve(question); } + private static Result happyEyeball(List r) { + Result result; + ExecutorService executor = (ExecutorService) Executors.newCachedThreadPool(); + + try { + result = executor.invokeAny(r); + executor.shutdown(); + return result; + } + catch (InterruptedException e) { + return null; + } + catch (ExecutionException e) { + return null; + } + + } + private static boolean validateHostname() { return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname); } - public static class Result implements Comparable { - public static final String DOMAIN = "domain"; - public static final String HOSTNAME = "hostname"; + public static class Result implements Comparable, Callable { + public static final String IP = "ip"; public static final String PORT = "port"; + public static final String HOSTNAME = "hostname"; public static final String PRIORITY = "priority"; public static final String DIRECT_TLS = "directTls"; public static final String AUTHENTICATED = "authenticated"; + private InetAddress ip; private DNSName hostname; private int port = DEFAULT_PORT_XMPP; private boolean directTls = false; private boolean authenticated = false; private int priority; + private Socket socket; - static Result fromRecord(SRV srv, boolean directTls) { - Result result = new Result(); - result.port = srv.port; - result.hostname = srv.name; - result.directTls = directTls; - result.priority = srv.priority; - return result; + public Result(DNSName hostname, InetAddress ip) { + this(hostname, ip, DEFAULT_PORT_XMPP); } - static Result createDefault(DNSName hostname) { - Result result = new Result(); - result.port = DEFAULT_PORT_XMPP; - result.hostname = hostname; - return result; + public Result(DNSName hostname, InetAddress ip, int port) { + this(hostname, ip, port, 0); } - public static Result fromCursor(Cursor cursor) { - final Result result = new Result(); - 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; - return result; + public Result(DNSName hostname, InetAddress ip, int port, int priority) { + this.hostname = hostname; + this.ip = ip; + this.port = port; + this.directTls = useDirectTls(port); + this.priority = priority; } @Override @@ -257,12 +325,14 @@ public class Resolver { 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; } @Override public int hashCode() { - int result = hostname != null ? hostname.hashCode() : 0; + 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); @@ -274,7 +344,7 @@ public class Resolver { return port; } - public DNSName getHostname() { + public DNSName getHostname() { return hostname; } @@ -286,9 +356,14 @@ public class Resolver { return authenticated; } + public Socket getSocket() { + return socket; + } + @Override public String toString() { return "Result{" + + "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' + ", hostame='" + hostname.toString() + '\'' + ", port=" + port + ", directTls=" + directTls + @@ -309,10 +384,22 @@ public class Resolver { return priority - result.priority; } } + @Override + public Result call() throws Exception { + final InetSocketAddress addr = new InetSocketAddress(ip, port); + final Socket localSocket = new Socket(); + localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + if(localSocket.isConnected()) { + this.socket = localSocket; + return this; + } + return null; + } public ContentValues toContentValues() { final ContentValues contentValues = new ContentValues(); contentValues.put(HOSTNAME, hostname == null ? null : hostname.toString()); + contentValues.put(IP, ip == null ? null : ip.getAddress()); contentValues.put(PORT, port); contentValues.put(PRIORITY, priority); contentValues.put(DIRECT_TLS, directTls ? 1 : 0); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 213c6b5aa..056c1353c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -290,65 +290,38 @@ public class XmppConnection implements Runnable { } } else { final String domain = account.getJid().getDomain(); - final List results; + final Resolver.Result result; final boolean hardcoded = extended && !account.getHostname().isEmpty(); if (hardcoded) { - results = Resolver.fromHardCoded(account.getHostname(), account.getPort()); + result = Resolver.fromHardCoded(account.getHostname(), account.getPort()); } else { - results = Resolver.resolve(domain); + result = Resolver.resolve(domain); } if (Thread.currentThread().isInterrupted()) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); return; } - if (results.size() == 0) { - Log.e(Config.LOGTAG,account.getJid().asBareJid()+": Resolver results were empty"); - return; + // if tls is true, encryption is implied and must not be started + features.encryptionEnabled = result.isDirectTls(); + verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; + Log.d(Config.LOGTAG,"verified hostname "+verifiedHostname); + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + + ": using values from resolver " + + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); + + localSocket = result.getSocket(); + + if (features.encryptionEnabled) { + localSocket = upgradeSocketToTls(localSocket); } - for (Iterator iterator = results.iterator(); iterator.hasNext(); ) { - final Resolver.Result result = iterator.next(); - if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); - return; - } - try { - // if tls is true, encryption is implied and must not be started - features.encryptionEnabled = result.isDirectTls(); - verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; - Log.d(Config.LOGTAG,"verified hostname "+verifiedHostname); - final InetSocketAddress addr = new InetSocketAddress(IDN.toASCII(result.getHostname().toString()), result.getPort()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": using values from resolver " - + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled); - localSocket = new Socket(); - localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - - if (features.encryptionEnabled) { - localSocket = upgradeSocketToTls(localSocket); - } - - localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); - if (startXmpp(localSocket)) { - localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this - break; // successfully connected to server that speaks xmpp - } else { - FileBackend.close(localSocket); - throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); - } - } catch (final StateChangingException e) { - if (!iterator.hasNext()) { - throw e; - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": thread was interrupted before beginning stream"); - return; - } catch (final Throwable e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")"); - if (!iterator.hasNext()) { - throw new UnknownHostException(); - } - } + localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); + if (startXmpp(localSocket)) { + localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this + return; // successfully connected to server that speaks xmpp + } else { + FileBackend.close(localSocket); + throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); } } processStream();