diff options
Diffstat (limited to 'src/main/java')
3 files changed, 130 insertions, 254 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java b/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java index 3bc79c4f..1e0eebc7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java +++ b/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java @@ -8,6 +8,7 @@ public class SrvRecord implements Comparable<SrvRecord> { private int priority; private String name; private int port; + private boolean useTls = false; public SrvRecord(int priority, String name, int port) { this.priority = priority; @@ -15,6 +16,13 @@ public class SrvRecord implements Comparable<SrvRecord> { this.port = port; } + public SrvRecord(int priority, String name, int port, boolean useTls) { + this.priority = priority; + this.name = name; + this.port = port; + this.useTls = useTls; + } + /** * Compares this record to the specified record to determine their relative * order. @@ -50,4 +58,8 @@ public class SrvRecord implements Comparable<SrvRecord> { public int getPriority() { return priority; } + + public boolean isUseTls() { + return useTls; + } } diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 58d53216..6da9c7e8 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -7,20 +7,12 @@ import android.net.LinkProperties; import android.net.Network; import android.net.RouteInfo; import android.os.Build; -import android.os.Bundle; -import android.os.Parcelable; -import android.util.Log; import java.io.IOException; import java.net.InetAddress; -import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Random; -import java.util.TreeMap; -import java.util.Map; import java.util.regex.Pattern; import de.measite.minidns.Client; @@ -28,58 +20,62 @@ import de.measite.minidns.DNSMessage; import de.measite.minidns.Record; import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; -import de.measite.minidns.Record.CLASS; import de.measite.minidns.record.SRV; -import de.measite.minidns.record.A; -import de.measite.minidns.record.AAAA; import de.measite.minidns.record.Data; -import de.measite.minidns.record.SRV; import de.measite.minidns.util.NameUtil; -import java.io.IOException; -import java.net.InetAddress; import java.util.TreeSet; -import java.util.regex.Pattern; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.dto.SrvRecord; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jid.Jid; public class DNSHelper { - - public static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - public static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + private static final String CLIENT_SRV_PREFIX = "_xmpp-client._tcp."; + private static final String SECURE_CLIENT_SRV_PREFIX = "_xmpps-client._tcp."; + private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); protected static Client client = new Client(); - public static Bundle getSRVRecord(final Jid jid, Context context) throws IOException { - final String host = jid.getDomainpart(); - final List<InetAddress> servers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDnsServers(context) : getDnsServersPreLollipop(); - Bundle b = new Bundle(); - for(InetAddress server : servers) { - b = queryDNS(host, server); - if (b.containsKey("values")) { - return b; - } - } - if (!b.containsKey("values")) { - Log.d(Config.LOGTAG,"all dns queries failed. provide fallback A record"); - ArrayList<Parcelable> values = new ArrayList<>(); - values.add(createNamePortBundle(host, 5222, false)); - b.putParcelableArrayList("values",values); - } - return b; - } + static { + client.setTimeout(Config.PING_TIMEOUT * 1000); + } + + /** + * Queries the SRV record for the server JID. + * This method uses all available Domain Name Servers. + * @param jid the server JID + * @return TreeSet with SrvRecords. If no SRV record is found for JID an empty TreeSet is returned. + */ + public static final TreeSet<SrvRecord> querySrvRecord(Jid jid) { + String host = jid.getDomainpart(); + TreeSet<SrvRecord> result = new TreeSet<>(); + + final List<InetAddress> dnsServers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDnsServers() : getDnsServersPreLollipop(); + + if (dnsServers != null) { + for (InetAddress dnsServer : dnsServers) { + result = querySrvRecord(host, dnsServer); + if (!result.isEmpty()) { + break; + } + } + } + + return result; + } @TargetApi(21) - private static List<InetAddress> getDnsServers(Context context) { + private static List<InetAddress> getDnsServers() { List<InetAddress> servers = new ArrayList<>(); - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + ConnectivityManager connectivityManager = (ConnectivityManager) ConversationsPlusApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks(); if (networks == null) { return getDnsServersPreLollipop(); @@ -95,7 +91,7 @@ public class DNSHelper { } } if (servers.size() > 0) { - Log.d(Config.LOGTAG, "used lollipop variant to discover dns servers in " + networks.length + " networks"); + Logging.d("dns", "used lollipop variant to discover dns servers in " + networks.length + " networks"); } return servers.size() > 0 ? servers : getDnsServersPreLollipop(); } @@ -123,155 +119,38 @@ public class DNSHelper { return servers; } - private static class TlsSrv { - private final SRV srv; - private final boolean tls; - - public TlsSrv(SRV srv, boolean tls) { - this.srv = srv; - this.tls = tls; - } - } - - private static void fillSrvMaps(final String qname, final InetAddress dnsServer, final Map<Integer, List<TlsSrv>> priorities, final Map<String, List<String>> ips4, final Map<String, List<String>> ips6, final boolean tls) throws IOException { - final DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); - for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { - for (Record rr : rrset) { - Data d = rr.getPayload(); - if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { - SRV srv = (SRV) d; - if (!priorities.containsKey(srv.getPriority())) { - priorities.put(srv.getPriority(),new ArrayList<TlsSrv>()); - } - priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls)); - } - if (d instanceof A) { - A a = (A) d; - if (!ips4.containsKey(rr.getName())) { - ips4.put(rr.getName(), new ArrayList<String>()); - } - ips4.get(rr.getName()).add(a.toString()); - } - if (d instanceof AAAA) { - AAAA aaaa = (AAAA) d; - if (!ips6.containsKey(rr.getName())) { - ips6.put(rr.getName(), new ArrayList<String>()); - } - ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); - } - } - } - } - - public static Bundle queryDNS(String host, InetAddress dnsServer) { - Bundle bundle = new Bundle(); - try { - client.setTimeout(Config.PING_TIMEOUT * 1000); - final String qname = "_xmpp-client._tcp." + host; - final String tlsQname = "_xmpps-client._tcp." + host; - Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); - - final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>(); - final Map<String, List<String>> ips4 = new TreeMap<>(); - final Map<String, List<String>> ips6 = new TreeMap<>(); - - fillSrvMaps(qname, dnsServer, priorities, ips4, ips6, false); - fillSrvMaps(tlsQname, dnsServer, priorities, ips4, ips6, true); - - final List<TlsSrv> result = new ArrayList<>(); - for (final List<TlsSrv> s : priorities.values()) { - result.addAll(s); - } - - final ArrayList<Bundle> values = new ArrayList<>(); - if (result.size() == 0) { - DNSMessage response; - try { - response = client.query(host, TYPE.A, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying A record on "+dnsServer.getHostAddress()); - } - try { - response = client.query(host, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); - } - values.add(createNamePortBundle(host, 5222, false)); - bundle.putParcelableArrayList("values", values); - return bundle; - } - for (final TlsSrv tlsSrv : result) { - final SRV srv = tlsSrv.srv; - if (ips6.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls)); - } else { - try { - DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); - } - } - if (ips4.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls)); - } else { - DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress()); - for(int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls)); - } - } - values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls)); - } - bundle.putParcelableArrayList("values", values); - } catch (SocketTimeoutException e) { - bundle.putString("error", "timeout"); - } catch (Exception e) { - bundle.putString("error", "unhandled"); - } - return bundle; - } - - private static Bundle createNamePortBundle(String name, int port, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - return namePort; - } - - private static Bundle createNamePortBundle(String name, int port, Map<String, List<String>> ips, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - if (ips!=null) { - List<String> ip = ips.get(name); - Collections.shuffle(ip, new Random()); - namePort.putString("ip", ip.get(0)); - } - return namePort; - } - - private static Bundle createNamePortBundle(String name, int port, Data data, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - if (data instanceof A) { - namePort.putString("ip", data.toString()); - } else if (data instanceof AAAA) { - namePort.putString("ip","["+data.toString()+"]"); - } - return namePort; - } + /** + * Queries the SRV record for an host from the given Domain Name Server. + * @param host the host to query for + * @param dnsServerAddress the DNS to query on + * @return TreeSet with SrvRecords. + */ + private static final TreeSet<SrvRecord> querySrvRecord(String host, InetAddress dnsServerAddress) { + TreeSet<SrvRecord> result = new TreeSet<>(); + querySrvRecord(host, dnsServerAddress, false, result); + querySrvRecord(host, dnsServerAddress, true, result); + return result; + } + + private static final void querySrvRecord(String host, InetAddress dnsServerAddress, boolean tlsSrvRecord, TreeSet<SrvRecord> result) { + String qname = (tlsSrvRecord ? SECURE_CLIENT_SRV_PREFIX : CLIENT_SRV_PREFIX) + host; + String dnsServerHostAddress = dnsServerAddress.getHostAddress(); + Logging.d("dns", "using dns server: " + dnsServerHostAddress + " to look up " + qname); + try { + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServerHostAddress); + Record[] rrset = message.getAnswers(); + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord); + result.add(srvRecord); + } + } + } catch (IOException e) { + Logging.d("dns", "Error while retrieving SRV record '" + qname + "' for '" + host + "' from DNS '" + dnsServerHostAddress + "': " + e.getMessage()); + } + } public static boolean isIp(final String server) { return server != null && ( diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index ab647a15..30d68a67 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -282,69 +282,54 @@ public class XmppConnection implements Runnable { } startXmpp(); } else { - final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService); - final ArrayList<Parcelable>values = result.getParcelableArrayList("values"); - for(Iterator<Parcelable> iterator = values.iterator(); iterator.hasNext();) { - final Bundle namePort = (Bundle) iterator.next(); - try { - String srvRecordServer; - try { - srvRecordServer = IDN.toASCII(namePort.getString("name")); - } catch (final IllegalArgumentException e) { - // TODO: Handle me?` - srvRecordServer = ""; - } - final int srvRecordPort = namePort.getInt("port"); - final String srvIpServer = namePort.getString("ip"); - // if tls is true, encryption is implied and must not be started - features.encryptionEnabled = namePort.getBoolean("tls"); - final InetSocketAddress addr; - if (srvIpServer != null) { - addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort + " tls: " + features.encryptionEnabled); - } else { - addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort + " tls: " + features.encryptionEnabled); - } - - if (!features.encryptionEnabled) { - socket = new Socket(); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - } else { - final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - socket = tlsFactoryVerifier.factory.createSocket(); - - if (socket == null) { - throw new IOException("could not initialize ssl socket"); - } - - SSLSocketHelper.setSecurity((SSLSocket) socket); - SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); - SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); - - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - - if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); - throw new SecurityException(); - } - } - - if (startXmpp()) - break; // successfully connected to server that speaks xmpp - } catch(final SecurityException e) { - throw e; - } catch (final Throwable e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() +"("+e.getClass().getName()+")"); - if (!iterator.hasNext()) { - throw new UnknownHostException(); - } - } - } + final TreeSet<SrvRecord> srvRecords = DNSHelper.querySrvRecord(account.getServer()); + if (srvRecords.isEmpty()) { + socket = new Socket(); + try { + socket.connect(new InetSocketAddress(account.getServer().getDomainpart(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000); + } catch (IOException e) { + throw new UnknownHostException(); + } + startXmpp(); + } else { + for (SrvRecord srvRecord : srvRecords) { + // if tls is true, encryption is implied and must not be started + features.encryptionEnabled = srvRecord.isUseTls(); + TlsFactoryVerifier tlsFactoryVerifier = null; + if (features.encryptionEnabled) { + try { + tlsFactoryVerifier = getTlsFactoryVerifier(); + socket = tlsFactoryVerifier.factory.createSocket(); + + if (socket == null) { + throw new IOException("could not initialize ssl socket"); + } + + SSLSocketHelper.setSecurity((SSLSocket) socket); + SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); + SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); + } catch (SecurityException e) { + throw e; + } catch (KeyManagementException e) { + Logging.e("connection-init", "Error while creating TLS verifier factory: " + e.getMessage(), e); + throw new SecurityException(); + } + } else { + socket = new Socket(); + } + + socket.connect(new InetSocketAddress(srvRecord.getName(), srvRecord.getPort()), Config.SOCKET_TIMEOUT * 1000); + + if (null != tlsFactoryVerifier && !tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); + throw new SecurityException(); + } + + if (startXmpp()) { + break; // successfully connected to server that speaks xmpp + } + } + } } processStream(); } catch (final IncompatibleServerException e) { |