JingleSocks5Transport.java

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