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