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}