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