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