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