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