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