make Tor connections work with direct TLS

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/utils/Resolver.java           |   6 
src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java |  22 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java      | 109 
3 files changed, 73 insertions(+), 64 deletions(-)

Detailed changes

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

@@ -76,12 +76,16 @@ public class Resolver {
         Result result = new Result();
         result.hostname = DNSName.from(hostname);
         result.port = port;
-        result.directTls = port == 443 || port == 5223;
+        result.directTls = useDirectTls(port);
         result.authenticated = true;
         return Collections.singletonList(result);
     }
 
 
+    public static boolean useDirectTls(final int port) {
+        return port == 443 || port == 5223;
+    }
+
     public static List<Result> resolve(String domain) {
         final  List<Result> ipResults = fromIpAddress(domain);
         if (ipResults.size() > 0) {

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

@@ -21,7 +21,7 @@ public class SocksSocketFactory {
 		byte[] response = new byte[2];
 		proxyIs.read(response);
 		if (response[0] != 0x05 || response[1] != 0x00) {
-			throw new SocksConnectionException();
+			throw new SocksConnectionException("Socks 5 handshake failed");
 		}
 		byte[] dest = destination.getBytes();
 		ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
@@ -33,7 +33,13 @@ public class SocksSocketFactory {
 		response = new byte[7 + dest.length];
 		proxyIs.read(response);
 		if (response[1] != 0x00) {
-			throw new SocksConnectionException();
+			if (response[1] == 0x04) {
+				throw new HostNotFoundException("Host unreachable");
+			}
+			if (response[1] == 0x05) {
+				throw new HostNotFoundException("Connection refused");
+			}
+			throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1]));
 		}
 	}
 
@@ -61,11 +67,19 @@ public class SocksSocketFactory {
 		return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
 	}
 
-	static class SocksConnectionException extends IOException {
-
+	private static class SocksConnectionException extends IOException {
+		SocksConnectionException(String message) {
+			super(message);
+		}
 	}
 
 	public static class SocksProxyNotFoundException extends IOException {
 
 	}
+
+	public static class HostNotFoundException extends SocksConnectionException {
+		HostNotFoundException(String message) {
+			super(message);
+		}
+	}
 }

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

@@ -267,8 +267,18 @@ public class XmppConnection implements Runnable {
                     destination = account.getHostname();
                     this.verifiedHostname = destination;
                 }
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor");
-                localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
+
+                final int port = account.getPort();
+                final boolean directTls = Resolver.useDirectTls(port);
+
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls="+directTls);
+                localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
+
+                if (directTls) {
+                    localSocket = upgradeSocketToTls(localSocket);
+                    features.encryptionEnabled = true;
+                }
+
                 try {
                     startXmpp(localSocket);
                 } catch (InterruptedException e) {
@@ -328,29 +338,13 @@ public class XmppConnection implements Runnable {
                                     + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
                         }
 
-                        if (!features.encryptionEnabled) {
-                            localSocket = new Socket();
-                            localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
-                        } else {
-                            final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
-                            localSocket = tlsFactoryVerifier.factory.createSocket();
-
-                            if (localSocket == null) {
-                                throw new IOException("could not initialize ssl socket");
-                            }
-
-                            SSLSocketHelper.setSecurity((SSLSocket) localSocket);
-                            SSLSocketHelper.setHostname((SSLSocket) localSocket, account.getServer());
-                            SSLSocketHelper.setApplicationProtocol((SSLSocket) localSocket, "xmpp-client");
+                        localSocket = new Socket();
+                        localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 
-                            localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
-
-                            if (!tlsFactoryVerifier.verifier.verify(account.getServer(), verifiedHostname, ((SSLSocket) localSocket).getSession())) {
-                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
-                                FileBackend.close(localSocket);
-                                throw new StateChangingException(Account.State.TLS_ERROR);
-                            }
+                        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
@@ -384,6 +378,8 @@ public class XmppConnection implements Runnable {
             this.changeStatus(e.state);
         } catch (final UnknownHostException | ConnectException e) {
             this.changeStatus(Account.State.SERVER_NOT_FOUND);
+        } catch (final SocksSocketFactory.HostNotFoundException e) {
+            this.changeStatus(Account.State.SERVER_NOT_FOUND);
         } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
             this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
         } catch (final IOException | XmlPullParserException  e) {
@@ -796,46 +792,41 @@ public class XmppConnection implements Runnable {
 
     private void switchOverToTls() throws XmlPullParserException, IOException {
         tagReader.readTag();
-        try {
-            final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
-            final InetAddress address = socket == null ? null : socket.getInetAddress();
-
-            if (address == null) {
-                throw new IOException("could not setup ssl");
-            }
-
-            final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
-
-
-            if (sslSocket == null) {
-                throw new IOException("could not initialize ssl socket");
-            }
-
-            SSLSocketHelper.setSecurity(sslSocket);
-            SSLSocketHelper.setHostname(sslSocket, account.getServer());
-            SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
+        final Socket socket = this.socket;
+        final SSLSocket sslSocket = upgradeSocketToTls(socket);
+        tagReader.setInputStream(sslSocket.getInputStream());
+        tagWriter.setOutputStream(sslSocket.getOutputStream());
+        sendStartStream();
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
+        features.encryptionEnabled = true;
+        final Tag tag = tagReader.readTag();
+        if (tag != null && tag.isStart("stream")) {
+            SSLSocketHelper.log(account, sslSocket);
+            processStream();
+        } else {
+            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
+        }
+        sslSocket.close();
+    }
 
-            if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
-                throw new StateChangingException(Account.State.TLS_ERROR);
-            }
-            tagReader.setInputStream(sslSocket.getInputStream());
-            tagWriter.setOutputStream(sslSocket.getOutputStream());
-            sendStartStream();
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
-            features.encryptionEnabled = true;
-            final Tag tag = tagReader.readTag();
-            if (tag != null && tag.isStart("stream")) {
-                SSLSocketHelper.log(account, sslSocket);
-                processStream();
-            } else {
-                throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
-            }
-            sslSocket.close();
-        } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
+    private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
+        final TlsFactoryVerifier tlsFactoryVerifier;
+        try {
+            tlsFactoryVerifier = getTlsFactoryVerifier();
+        } catch (final NoSuchAlgorithmException | KeyManagementException e) {
+            throw new StateChangingException(Account.State.TLS_ERROR);
+        }
+        final InetAddress address = socket.getInetAddress();
+        final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
+        SSLSocketHelper.setSecurity(sslSocket);
+        SSLSocketHelper.setHostname(sslSocket, account.getServer());
+        SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
+        if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
+            FileBackend.close(sslSocket);
             throw new StateChangingException(Account.State.TLS_ERROR);
         }
+        return sslSocket;
     }
 
     private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException {