JingleSocks5Transport.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.os.PowerManager;
  4import android.util.Log;
  5
  6import java.io.IOException;
  7import java.io.InputStream;
  8import java.io.OutputStream;
  9import java.net.InetAddress;
 10import java.net.InetSocketAddress;
 11import java.net.ServerSocket;
 12import java.net.Socket;
 13import java.net.SocketAddress;
 14import java.nio.ByteBuffer;
 15import java.security.MessageDigest;
 16import java.security.NoSuchAlgorithmException;
 17
 18import eu.siacs.conversations.Config;
 19import eu.siacs.conversations.entities.Account;
 20import eu.siacs.conversations.entities.DownloadableFile;
 21import eu.siacs.conversations.persistance.FileBackend;
 22import eu.siacs.conversations.services.AbstractConnectionManager;
 23import eu.siacs.conversations.utils.CryptoHelper;
 24import eu.siacs.conversations.utils.SocksSocketFactory;
 25import eu.siacs.conversations.utils.WakeLockHelper;
 26import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 27
 28public class JingleSocks5Transport extends JingleTransport {
 29
 30    private static final int SOCKET_TIMEOUT_DIRECT = 3000;
 31    private static final int SOCKET_TIMEOUT_PROXY = 5000;
 32
 33    private final JingleCandidate candidate;
 34    private final JingleFileTransferConnection connection;
 35    private final String destination;
 36    private final Account account;
 37    private OutputStream outputStream;
 38    private InputStream inputStream;
 39    private boolean isEstablished = false;
 40    private boolean activated = false;
 41    private ServerSocket serverSocket;
 42    private Socket socket;
 43
 44    JingleSocks5Transport(JingleFileTransferConnection jingleConnection, JingleCandidate candidate) {
 45        final MessageDigest messageDigest;
 46        try {
 47            messageDigest = MessageDigest.getInstance("SHA-1");
 48        } catch (NoSuchAlgorithmException e) {
 49            throw new AssertionError(e);
 50        }
 51        this.candidate = candidate;
 52        this.connection = jingleConnection;
 53        this.account = jingleConnection.getId().account;
 54        final StringBuilder destBuilder = new StringBuilder();
 55        if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) {
 56            Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
 57            destBuilder.append(this.connection.getId().sessionId);
 58        } else {
 59            destBuilder.append(this.connection.getTransportId());
 60        }
 61        if (candidate.getType() == JingleCandidate.TYPE_PROXY) {
 62            if (candidate.isOurs()) {
 63                destBuilder.append(this.account.getJid());
 64                destBuilder.append(this.connection.getId().with);
 65            } else {
 66                destBuilder.append(this.connection.getId().with);
 67                destBuilder.append(this.account.getJid());
 68            }
 69        } else {
 70            if (connection.isInitiator()) {
 71                destBuilder.append(this.account.getJid());
 72                destBuilder.append(this.connection.getId().with);
 73            } else {
 74                destBuilder.append(this.connection.getId().with);
 75                destBuilder.append(this.account.getJid());
 76            }
 77        }
 78        messageDigest.reset();
 79        this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
 80        if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) {
 81            createServerSocket();
 82        }
 83    }
 84
 85    private void createServerSocket() {
 86        try {
 87            serverSocket = new ServerSocket();
 88            serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort()));
 89            new Thread(() -> {
 90                try {
 91                    final Socket socket = serverSocket.accept();
 92                    new Thread(() -> {
 93                        try {
 94                            acceptIncomingSocketConnection(socket);
 95                        } catch (IOException e) {
 96                            Log.d(Config.LOGTAG, "unable to read from socket", e);
 97
 98                        }
 99                    }).start();
100                } catch (IOException e) {
101                    if (!serverSocket.isClosed()) {
102                        Log.d(Config.LOGTAG, "unable to accept socket", e);
103                    }
104                }
105            }).start();
106        } catch (IOException e) {
107            Log.d(Config.LOGTAG, "unable to bind server socket ", e);
108        }
109    }
110
111    private void acceptIncomingSocketConnection(final Socket socket) throws IOException {
112        Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
113        socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT);
114        final byte[] authBegin = new byte[2];
115        final InputStream inputStream = socket.getInputStream();
116        final OutputStream outputStream = socket.getOutputStream();
117        inputStream.read(authBegin);
118        if (authBegin[0] != 0x5) {
119            socket.close();
120        }
121        final short methodCount = authBegin[1];
122        final byte[] methods = new byte[methodCount];
123        inputStream.read(methods);
124        if (SocksSocketFactory.contains((byte) 0x00, methods)) {
125            outputStream.write(new byte[]{0x05, 0x00});
126        } else {
127            outputStream.write(new byte[]{0x05, (byte) 0xff});
128        }
129        byte[] connectCommand = new byte[4];
130        inputStream.read(connectCommand);
131        if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
132            int destinationCount = inputStream.read();
133            final byte[] destination = new byte[destinationCount];
134            inputStream.read(destination);
135            final byte[] port = new byte[2];
136            inputStream.read(port);
137            final String receivedDestination = new String(destination);
138            final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
139            final byte[] responseHeader;
140            final boolean success;
141            if (receivedDestination.equals(this.destination) && this.socket == null) {
142                responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
143                success = true;
144            } else {
145                Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")");
146                responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
147                success = false;
148            }
149            response.put(responseHeader);
150            response.put((byte) destination.length);
151            response.put(destination);
152            response.put(port);
153            outputStream.write(response.array());
154            outputStream.flush();
155            if (success) {
156                Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort());
157                socket.setSoTimeout(0);
158                this.socket = socket;
159                this.inputStream = inputStream;
160                this.outputStream = outputStream;
161                this.isEstablished = true;
162                FileBackend.close(serverSocket);
163            } else {
164                FileBackend.close(socket);
165            }
166        } else {
167            socket.close();
168        }
169    }
170
171    public void connect(final OnTransportConnected callback) {
172        new Thread(() -> {
173            final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY;
174            try {
175                final boolean useTor = this.account.isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect();
176                if (useTor) {
177                    socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort());
178                } else {
179                    socket = new Socket();
180                    SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
181                    socket.connect(address, timeout);
182                }
183                inputStream = socket.getInputStream();
184                outputStream = socket.getOutputStream();
185                socket.setSoTimeout(timeout);
186                SocksSocketFactory.createSocksConnection(socket, destination, 0);
187                socket.setSoTimeout(0);
188                isEstablished = true;
189                callback.established();
190            } catch (IOException e) {
191                callback.failed();
192            }
193        }).start();
194
195    }
196
197    public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
198        new Thread(() -> {
199            InputStream fileInputStream = null;
200            final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getId().sessionId);
201            long transmitted = 0;
202            try {
203                wakeLock.acquire();
204                MessageDigest digest = MessageDigest.getInstance("SHA-1");
205                digest.reset();
206                fileInputStream = connection.getFileInputStream();
207                if (fileInputStream == null) {
208                    Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create input stream");
209                    callback.onFileTransferAborted();
210                    return;
211                }
212                final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
213                long size = file.getExpectedSize();
214                int count;
215                byte[] buffer = new byte[8192];
216                while ((count = innerInputStream.read(buffer)) > 0) {
217                    outputStream.write(buffer, 0, count);
218                    digest.update(buffer, 0, count);
219                    transmitted += count;
220                    connection.updateProgress((int) ((((double) transmitted) / size) * 100));
221                }
222                outputStream.flush();
223                file.setSha1Sum(digest.digest());
224                if (callback != null) {
225                    callback.onFileTransmitted(file);
226                }
227            } catch (Exception e) {
228                final Account account = this.account;
229                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e);
230                callback.onFileTransferAborted();
231            } finally {
232                FileBackend.close(fileInputStream);
233                WakeLockHelper.release(wakeLock);
234            }
235        }).start();
236
237    }
238
239    public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
240        new Thread(() -> {
241            OutputStream fileOutputStream = null;
242            final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getId().sessionId);
243            try {
244                wakeLock.acquire();
245                MessageDigest digest = MessageDigest.getInstance("SHA-1");
246                digest.reset();
247                //inputStream.skip(45);
248                socket.setSoTimeout(30000);
249                fileOutputStream = connection.getFileOutputStream();
250                if (fileOutputStream == null) {
251                    callback.onFileTransferAborted();
252                    Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create output stream");
253                    return;
254                }
255                double size = file.getExpectedSize();
256                long remainingSize = file.getExpectedSize();
257                byte[] buffer = new byte[8192];
258                int count;
259                while (remainingSize > 0) {
260                    count = inputStream.read(buffer);
261                    if (count == -1) {
262                        callback.onFileTransferAborted();
263                        Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining");
264                        return;
265                    } else {
266                        fileOutputStream.write(buffer, 0, count);
267                        digest.update(buffer, 0, count);
268                        remainingSize -= count;
269                    }
270                    connection.updateProgress((int) (((size - remainingSize) / size) * 100));
271                }
272                fileOutputStream.flush();
273                fileOutputStream.close();
274                file.setSha1Sum(digest.digest());
275                callback.onFileTransmitted(file);
276            } catch (Exception e) {
277                Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": " + e.getMessage());
278                callback.onFileTransferAborted();
279            } finally {
280                WakeLockHelper.release(wakeLock);
281                FileBackend.close(fileOutputStream);
282                FileBackend.close(inputStream);
283            }
284        }).start();
285    }
286
287    public boolean isProxy() {
288        return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
289    }
290
291    public boolean needsActivation() {
292        return (this.isProxy() && !this.activated);
293    }
294
295    public void disconnect() {
296        FileBackend.close(inputStream);
297        FileBackend.close(outputStream);
298        FileBackend.close(socket);
299        FileBackend.close(serverSocket);
300    }
301
302    public boolean isEstablished() {
303        return this.isEstablished;
304    }
305
306    public JingleCandidate getCandidate() {
307        return this.candidate;
308    }
309
310    public void setActivated(boolean activated) {
311        this.activated = activated;
312    }
313}