bare minimum direct connections

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/persistance/FileBackend.java           | 10 
src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java          | 12 
src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java | 52 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java       |  4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java      | 18 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java | 89 
6 files changed, 178 insertions(+), 7 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -39,6 +39,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.URL;
 import java.security.DigestOutputStream;
@@ -359,6 +360,15 @@ public class FileBackend {
         }
     }
 
+    public static void close(final ServerSocket socket) {
+        if (socket != null) {
+            try {
+                socket.close();
+            } catch (IOException e) {
+            }
+        }
+    }
+
     public static boolean weOwnFile(Context context, Uri uri) {
         if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
             return false;

src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java 🔗

@@ -20,6 +20,9 @@ public class SocksSocketFactory {
 		proxyOs.write(new byte[]{0x05, 0x01, 0x00});
 		byte[] response = new byte[2];
 		proxyIs.read(response);
+		if (response[0] != 0x05 || response[1] != 0x00) {
+			throw new SocksConnectionException();
+		}
 		byte[] dest = destination.getBytes();
 		ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
 		request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
@@ -34,6 +37,15 @@ public class SocksSocketFactory {
 		}
 	}
 
+	public static boolean contains(byte needle, byte[] haystack) {
+		for(byte hay : haystack) {
+			if (hay == needle) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
 		Socket socket = new Socket();
 		try {

src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java 🔗

@@ -0,0 +1,52 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.UUID;
+
+import rocks.xmpp.addr.Jid;
+
+public class DirectConnectionUtils {
+
+    private static List<InetAddress> getLocalAddresses() {
+        final List<InetAddress> addresses = new ArrayList<>();
+        final Enumeration<NetworkInterface> interfaces;
+        try {
+            interfaces = NetworkInterface.getNetworkInterfaces();
+        } catch (SocketException e) {
+            return addresses;
+        }
+        while (interfaces.hasMoreElements()) {
+            NetworkInterface networkInterface = interfaces.nextElement();
+            final Enumeration<InetAddress> inetAddressEnumeration = networkInterface.getInetAddresses();
+            while (inetAddressEnumeration.hasMoreElements()) {
+                final InetAddress inetAddress = inetAddressEnumeration.nextElement();
+                if (!inetAddress.isLoopbackAddress()) {
+                    addresses.add(inetAddress);
+                }
+            }
+        }
+        return addresses;
+    }
+
+    public static List<JingleCandidate> getLocalCandidates(Jid jid) {
+        SecureRandom random = new SecureRandom();
+        ArrayList<JingleCandidate> candidates = new ArrayList<>();
+        for (InetAddress inetAddress : getLocalAddresses()) {
+            final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
+            candidate.setHost(inetAddress.getHostAddress());
+            candidate.setPort(random.nextInt(60000) + 1024);
+            candidate.setType(JingleCandidate.TYPE_DIRECT);
+            candidate.setJid(jid);
+            candidate.setPriority(8257536 + candidates.size());
+            candidates.add(candidate);
+        }
+        return candidates;
+    }
+
+}

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java 🔗

@@ -127,7 +127,9 @@ public class JingleCandidate {
 		element.setAttribute("cid", this.getCid());
 		element.setAttribute("host", this.getHost());
 		element.setAttribute("port", Integer.toString(this.getPort()));
-		element.setAttribute("jid", this.getJid().toString());
+		if (jid != null) {
+			element.setAttribute("jid", jid.toEscapedString());
+		}
 		element.setAttribute("priority", Integer.toString(this.getPriority()));
 		if (this.getType() == TYPE_DIRECT) {
 			element.setAttribute("type", "direct");

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java 🔗

@@ -11,7 +11,6 @@ import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
@@ -303,9 +302,15 @@ public class JingleConnection implements Transferable {
         this.transportId = this.mJingleConnectionManager.nextRandomId();
         if (this.initialTransport == Transport.IBB) {
             this.sendInitRequest();
-        } else if (this.candidates.size() > 0) {
-            this.sendInitRequest(); //TODO we will never get here? Can probably be removed
         } else {
+
+            final List<JingleCandidate> directCandidates = DirectConnectionUtils.getLocalCandidates(account.getJid());
+            for (JingleCandidate directCandidate : directCandidates) {
+                final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
+                connections.put(directCandidate.getCid(), socksConnection);
+                candidates.add(directCandidate);
+            }
+
             this.mJingleConnectionManager.getPrimaryCandidate(account, (success, candidate) -> {
                 if (success) {
                     final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
@@ -690,7 +695,7 @@ public class JingleConnection implements Transferable {
                 onProxyActivated.failed();
                 return true;
             } else if (content.socks5transport().hasChild("candidate-error")) {
-                Log.d(Config.LOGTAG, "received candidate error");
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error");
                 this.receivedCandidate = true;
                 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
                     this.connect();
@@ -728,7 +733,7 @@ public class JingleConnection implements Transferable {
         final JingleSocks5Transport connection = chooseConnection();
         this.transport = connection;
         if (connection == null) {
-            Log.d(Config.LOGTAG, "could not find suitable candidate");
+            Log.d(Config.LOGTAG, account.getJid().asBareJid()+": could not find suitable candidate");
             this.disconnectSocks5Connections();
             if (initiating()) {
                 this.sendFallbackToIbb();
@@ -755,6 +760,7 @@ public class JingleConnection implements Transferable {
                             .setContent(this.getCounterPart().toString());
                     mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> {
                         if (response.getType() != IqPacket.TYPE.RESULT) {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + response.toString());
                             onProxyActivated.failed();
                         } else {
                             onProxyActivated.success();
@@ -1052,7 +1058,7 @@ public class JingleConnection implements Transferable {
     }
 
     private void sendCandidateError() {
-        Log.d(Config.LOGTAG, "sending candidate error");
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending candidate error");
         JinglePacket packet = bootstrapPacket("transport-info");
         Content content = new Content(this.contentCreator, this.contentName);
         content.setTransportId(this.transportId);

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java 🔗

@@ -6,9 +6,12 @@ import android.util.Log;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketAddress;
+import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -29,6 +32,7 @@ public class JingleSocks5Transport extends JingleTransport {
     private InputStream inputStream;
     private boolean isEstablished = false;
     private boolean activated = false;
+    private ServerSocket serverSocket;
     private Socket socket;
 
     JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) {
@@ -56,6 +60,88 @@ public class JingleSocks5Transport extends JingleTransport {
         }
         messageDigest.reset();
         this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
+        if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) {
+            createServerSocket();
+        }
+    }
+
+    private void createServerSocket() {
+        try {
+            serverSocket = new ServerSocket();
+            serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort()));
+            new Thread(() -> {
+                try {
+                    final Socket socket = serverSocket.accept();
+                    new Thread(() -> {
+                        try {
+                            acceptIncomingSocketConnection(socket);
+                        } catch (IOException e) {
+                            Log.d(Config.LOGTAG,"unable to read from socket",e);
+
+                        }
+                    }).start();
+                } catch (IOException e) {
+                    if (!serverSocket.isClosed()) {
+                        Log.d(Config.LOGTAG, "unable to accept socket", e);
+                    }
+                }
+            }).start();
+        } catch (IOException e) {
+            Log.d(Config.LOGTAG,"unable to bind server socket ",e);
+        }
+    }
+
+    private void acceptIncomingSocketConnection(Socket socket) throws IOException {
+        Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
+        byte[] authBegin = new byte[2];
+        InputStream inputStream = socket.getInputStream();
+        OutputStream outputStream = socket.getOutputStream();
+        inputStream.read(authBegin);
+        if (authBegin[0] != 0x5) {
+            socket.close();
+        }
+        short methodCount = authBegin[1];
+        byte[] methods = new byte[methodCount];
+        inputStream.read(methods);
+        if (SocksSocketFactory.contains((byte) 0x00, methods)) {
+            outputStream.write(new byte[]{0x05,0x00});
+        } else {
+            outputStream.write(new byte[]{0x05,(byte) 0xff});
+        }
+        byte[] connectCommand = new byte[4];
+        inputStream.read(connectCommand);
+        if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
+            int destinationCount = inputStream.read();
+            byte[] destination = new byte[destinationCount];
+            inputStream.read(destination);
+            int port = inputStream.read();
+            final String receivedDestination = new String(destination);
+            Log.d(Config.LOGTAG, "received destination " + receivedDestination + ":" + port + " - expected " + this.destination);
+            final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
+            final byte[] responseHeader;
+            final boolean success;
+            if (receivedDestination.equals(this.destination)) {
+                responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
+                success = true;
+            } else {
+                responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
+                success = false;
+            }
+            response.put(responseHeader);
+            response.put((byte) destination.length);
+            response.put(destination);
+            response.putShort((short) port);
+            outputStream.write(response.array());
+            outputStream.flush();
+            if (success) {
+                this.socket = socket;
+                this.inputStream = inputStream;
+                this.outputStream = outputStream;
+                this.isEstablished = true;
+            }
+        } else {
+            socket.close();
+        }
     }
 
     public void connect(final OnTransportConnected callback) {
@@ -71,7 +157,9 @@ public class JingleSocks5Transport extends JingleTransport {
                 }
                 inputStream = socket.getInputStream();
                 outputStream = socket.getOutputStream();
+                socket.setSoTimeout(5000);
                 SocksSocketFactory.createSocksConnection(socket, destination, 0);
+                socket.setSoTimeout(0);
                 isEstablished = true;
                 callback.established();
             } catch (IOException e) {
@@ -182,6 +270,7 @@ public class JingleSocks5Transport extends JingleTransport {
         FileBackend.close(inputStream);
         FileBackend.close(outputStream);
         FileBackend.close(socket);
+        FileBackend.close(serverSocket);
     }
 
     public boolean isEstablished() {