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