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