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