make see-other-host and resume location work over Tor

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/utils/IP.java                 | 43 
src/main/java/eu/siacs/conversations/utils/Resolver.java           | 36 
src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java | 69 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java      | 58 
4 files changed, 132 insertions(+), 74 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/utils/IP.java πŸ”—

@@ -1,20 +1,30 @@
 package eu.siacs.conversations.utils;
 
 import com.google.common.net.InetAddresses;
-
+import java.net.InetAddress;
 import java.util.regex.Pattern;
 
 public class IP {
 
-    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");
-
-    public static boolean matches(String server) {
-        return server != null && (
-                PATTERN_IPV4.matcher(server).matches()
+    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");
+
+    public static boolean matches(final String server) {
+        return server != null
+                && (PATTERN_IPV4.matcher(server).matches()
                         || PATTERN_IPV6.matcher(server).matches()
                         || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
                         || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
@@ -22,8 +32,14 @@ public class IP {
     }
 
     public static String wrapIPv6(final String host) {
-        if (matches(host)) {
-            return String.format("[%s]", host);
+        if (InetAddresses.isInetAddress(host)) {
+            final InetAddress inetAddress;
+            try {
+                inetAddress = InetAddresses.forString(host);
+            } catch (final IllegalArgumentException e) {
+                return host;
+            }
+            return InetAddresses.toUriString(inetAddress);
         } else {
             return host;
         }
@@ -31,12 +47,11 @@ public class IP {
 
     public static String unwrapIPv6(final String host) {
         if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
-            final String ip = host.substring(1,host.length() -1);
+            final String ip = host.substring(1, host.length() - 1);
             if (InetAddresses.isInetAddress(ip)) {
                 return ip;
             }
         }
         return host;
     }
-
 }

src/main/java/eu/siacs/conversations/utils/Resolver.java πŸ”—

@@ -3,9 +3,7 @@ package eu.siacs.conversations.utils;
 import android.content.ContentValues;
 import android.database.Cursor;
 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;
@@ -18,26 +16,11 @@ 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.dnsname.InvalidDnsNameException;
-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.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -49,6 +32,17 @@ import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsname.DnsName;
+import org.minidns.dnsname.InvalidDnsNameException;
+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;
 
 public class Resolver {
 
@@ -156,8 +150,8 @@ public class Resolver {
         if (IP.matches(domain)) {
             final InetAddress inetAddress;
             try {
-                inetAddress = InetAddress.getByName(domain);
-            } catch (final UnknownHostException e) {
+                inetAddress = InetAddresses.forString(domain);
+            } catch (final IllegalArgumentException e) {
                 return Collections.emptyList();
             }
             return Result.createWithDefaultPorts(null, inetAddress);
@@ -442,6 +436,10 @@ public class Resolver {
                     .toString();
         }
 
+        public String asDestination() {
+            return ip != null ? InetAddresses.toAddrString(ip) : hostname.toString();
+        }
+
         public ContentValues toContentValues() {
             final ContentValues contentValues = new ContentValues();
             contentValues.put(IP, ip == null ? null : ip.getAddress());

src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java πŸ”—

@@ -1,37 +1,56 @@
 package eu.siacs.conversations.utils;
 
 import com.google.common.io.ByteStreams;
-
+import com.google.common.net.InetAddresses;
+import eu.siacs.conversations.Config;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.nio.ByteBuffer;
 
-import eu.siacs.conversations.Config;
-
 public class SocksSocketFactory {
 
-    private static final byte[] LOCALHOST = new byte[]{127, 0, 0, 1};
+    private static final byte[] LOCALHOST = new byte[] {127, 0, 0, 1};
 
-    public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException {
-        //TODO use different Socks Addr Type if destination is IP or IPv6
+    public static void createSocksConnection(
+            final Socket socket, final String destination, final int port) throws IOException {
         final InputStream proxyIs = socket.getInputStream();
         final OutputStream proxyOs = socket.getOutputStream();
-        proxyOs.write(new byte[]{0x05, 0x01, 0x00});
+        proxyOs.write(new byte[] {0x05, 0x01, 0x00});
         proxyOs.flush();
         final byte[] handshake = new byte[2];
         ByteStreams.readFully(proxyIs, handshake);
         if (handshake[0] != 0x05 || handshake[1] != 0x00) {
             throw new SocksConnectionException("Socks 5 handshake failed");
         }
-        final byte[] dest = destination.getBytes();
-        final ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
-        request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
-        request.put((byte) dest.length);
-        request.put(dest);
+        final byte type;
+        final ByteBuffer request;
+        if (InetAddresses.isInetAddress(destination)) {
+            final var ip = InetAddresses.forString(destination);
+            final var dest = ip.getAddress();
+            request = ByteBuffer.allocate(6 + dest.length);
+            if (ip instanceof Inet4Address) {
+                type = 0x01;
+            } else if (ip instanceof Inet6Address) {
+                type = 0x04;
+            } else {
+                throw new IOException("IP address is of unknown subtype");
+            }
+            request.put(new byte[] {0x05, 0x01, 0x00, type});
+            request.put(dest);
+        } else {
+            final byte[] dest = destination.getBytes();
+            type = 0x03;
+            request = ByteBuffer.allocate(7 + dest.length);
+            request.put(new byte[] {0x05, 0x01, 0x00, type});
+            request.put((byte) dest.length);
+            request.put(dest);
+        }
         request.putShort((short) port);
         proxyOs.write(request.array());
         proxyOs.flush();
@@ -42,13 +61,16 @@ public class SocksSocketFactory {
             throw new IOException(String.format("Unknown Socks version %02X ", ver));
         }
         final byte status = response[1];
-        final byte bndAddrType = response[3];
-        final byte[] bndDestination = readDestination(bndAddrType, proxyIs);
+        final byte bndAddressType = response[3];
+        final byte[] bndDestination = readDestination(bndAddressType, proxyIs);
         final byte[] bndPort = new byte[2];
-        if (bndAddrType == 0x03) {
+        if (bndAddressType == 0x03) {
             final String receivedDestination = new String(bndDestination);
             if (!receivedDestination.equalsIgnoreCase(destination)) {
-                throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination));
+                throw new IOException(
+                        String.format(
+                                "Destination mismatch. Received %s Expected %s",
+                                receivedDestination, destination));
             }
         }
         ByteStreams.readFully(proxyIs, bndPort);
@@ -63,7 +85,8 @@ public class SocksSocketFactory {
         }
     }
 
-    private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException {
+    private static byte[] readDestination(final byte type, final InputStream inputStream)
+            throws IOException {
         final byte[] bndDestination;
         if (type == 0x01) {
             bndDestination = new byte[4];
@@ -88,7 +111,8 @@ public class SocksSocketFactory {
         return false;
     }
 
-    private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
+    private static Socket createSocket(InetSocketAddress address, String destination, int port)
+            throws IOException {
         Socket socket = new Socket();
         try {
             socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
@@ -100,7 +124,10 @@ public class SocksSocketFactory {
     }
 
     public static Socket createSocketOverTor(String destination, int port) throws IOException {
-        return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
+        return createSocket(
+                new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050),
+                destination,
+                port);
     }
 
     private static class SocksConnectionException extends IOException {
@@ -109,9 +136,7 @@ public class SocksSocketFactory {
         }
     }
 
-    public static class SocksProxyNotFoundException extends IOException {
-
-    }
+    public static class SocksProxyNotFoundException extends IOException {}
 
     public static class HostNotFoundException extends SocksConnectionException {
         HostNotFoundException(String message) {

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java πŸ”—

@@ -19,6 +19,7 @@ import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
@@ -290,9 +291,9 @@ public class XmppConnection implements Runnable {
         this.quickStartInProgress = false;
         this.isBound = false;
         this.attempt++;
-        this.verifiedHostname =
-                null; // will be set if user entered hostname is being used or hostname was verified
-        // with dnssec
+        this.currentResolverResult = null;
+        // will be set if user entered hostname is being used or hostname was verified with dnssec
+        this.verifiedHostname = null;
         try {
             Socket localSocket;
             shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
@@ -300,33 +301,53 @@ public class XmppConnection implements Runnable {
             final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
             final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
             if (useTor) {
-                String destination;
-                if (account.getHostname().isEmpty() || account.isOnion()) {
-                    destination = account.getServer();
+                final var seeOtherHost = this.seeOtherHostResolverResult;
+                final Resolver.Result resume = streamId == null ? null : streamId.location;
+                final Resolver.Result viaTor;
+                if (account.isOnion()) {
+                    // for .onion JIDs we always connect to the onion address no matter what
+                    viaTor =
+                            Iterables.getOnlyElement(
+                                    Resolver.fromHardCoded(
+                                            account.getServer(), Resolver.XMPP_PORT_STARTTLS));
+                } else if (resume != null) {
+                    viaTor = resume;
+                } else if (seeOtherHost != null) {
+                    viaTor = seeOtherHost;
+                } else if (account.getHostname().isEmpty()) {
+                    viaTor =
+                            Iterables.getOnlyElement(
+                                    Resolver.fromHardCoded(
+                                            account.getServer(), Resolver.XMPP_PORT_STARTTLS));
                 } else {
-                    destination = account.getHostname();
-                    this.verifiedHostname = destination;
+                    viaTor =
+                            Iterables.getOnlyElement(
+                                    Resolver.fromHardCoded(
+                                            account.getHostname(), account.getPort()));
+                    this.verifiedHostname = account.getHostname();
                 }
 
-                final int port = account.getPort();
-                final boolean directTls = Resolver.useDirectTls(port);
-
                 Log.d(
                         Config.LOGTAG,
                         account.getJid().asBareJid()
                                 + ": connect to "
-                                + destination
+                                + viaTor.asDestination()
                                 + " via Tor. directTls="
-                                + directTls);
-                localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
+                                + viaTor.isDirectTls());
+                localSocket =
+                        SocksSocketFactory.createSocketOverTor(
+                                viaTor.asDestination(), viaTor.getPort());
 
-                if (directTls) {
+                if (viaTor.isDirectTls()) {
                     localSocket = upgradeSocketToTls(localSocket);
                     features.encryptionEnabled = true;
                 }
 
                 try {
-                    startXmpp(localSocket);
+                    if (startXmpp(localSocket)) {
+                        this.currentResolverResult = viaTor;
+                        this.seeOtherHostResolverResult = null;
+                    }
                 } catch (final InterruptedException e) {
                     Log.d(
                             Config.LOGTAG,
@@ -442,9 +463,8 @@ public class XmppConnection implements Runnable {
 
                         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
+                            // reset to 0; once the connection is established we don't want this
+                            localSocket.setSoTimeout(0);
                             if (!hardcoded && !result.equals(storedBackupResult)) {
                                 mXmppConnectionService.databaseBackend.saveResolverResult(
                                         domain, result);