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