diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java index 948f7537ad8f6af1dc90d71691b21b964548f938..8b07c98f66eadfa8e259e6458303d4314c177ad5 100644 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ b/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; } - } diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 224a5758f9fd8f9ab6177e1de908e6b8bbe21b29..a283d8496db75ffeb36b5debc4d7775e20635207 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/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()); diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 2b9d42d7ad1c53215604d752ef24953555344555..81ad58e98be22f36329565c74abfc66fba37051b 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 1da0963a31ccb4d29c445ca15f23156365ade5ac..3bba58c49b2c9c258c5dce3dd20aea3370f15208 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/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);