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