From 1c413edf0686f0347cd113116f5e1485016672b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 1 Sep 2019 15:06:59 +0200 Subject: [PATCH] bare minimum direct connections --- .../persistance/FileBackend.java | 10 +++ .../utils/SocksSocketFactory.java | 12 +++ .../xmpp/jingle/DirectConnectionUtils.java | 52 +++++++++++ .../xmpp/jingle/JingleCandidate.java | 4 +- .../xmpp/jingle/JingleConnection.java | 18 ++-- .../xmpp/jingle/JingleSocks5Transport.java | 89 +++++++++++++++++++ 6 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 8c77bdad4..509d4b193 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/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; diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 81f936538..eab9acfcb 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/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 { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java new file mode 100644 index 000000000..41a9455b7 --- /dev/null +++ b/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 getLocalAddresses() { + final List addresses = new ArrayList<>(); + final Enumeration interfaces; + try { + interfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return addresses; + } + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + final Enumeration inetAddressEnumeration = networkInterface.getInetAddresses(); + while (inetAddressEnumeration.hasMoreElements()) { + final InetAddress inetAddress = inetAddressEnumeration.nextElement(); + if (!inetAddress.isLoopbackAddress()) { + addresses.add(inetAddress); + } + } + } + return addresses; + } + + public static List getLocalCandidates(Jid jid) { + SecureRandom random = new SecureRandom(); + ArrayList 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; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java index d48804691..3f69980aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ b/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"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index e229c11ed..a9ac6e7df 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/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 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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 4181335f6..2db161271 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/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() {