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