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 byte[] authBegin = new byte[2];
97 InputStream inputStream = socket.getInputStream();
98 OutputStream outputStream = socket.getOutputStream();
99 inputStream.read(authBegin);
100 if (authBegin[0] != 0x5) {
101 socket.close();
102 }
103 short methodCount = authBegin[1];
104 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 byte[] destination = new byte[destinationCount];
116 inputStream.read(destination);
117 int port = inputStream.read();
118 final String receivedDestination = new String(destination);
119 Log.d(Config.LOGTAG, "received destination " + receivedDestination + ":" + port + " - expected " + this.destination);
120 final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
121 final byte[] responseHeader;
122 final boolean success;
123 if (receivedDestination.equals(this.destination)) {
124 responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
125 success = true;
126 } else {
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 }
142 } else {
143 socket.close();
144 }
145 }
146
147 public void connect(final OnTransportConnected callback) {
148 new Thread(() -> {
149 try {
150 final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect();
151 if (useTor) {
152 socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort());
153 } else {
154 socket = new Socket();
155 SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
156 socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
157 }
158 inputStream = socket.getInputStream();
159 outputStream = socket.getOutputStream();
160 socket.setSoTimeout(5000);
161 SocksSocketFactory.createSocksConnection(socket, destination, 0);
162 socket.setSoTimeout(0);
163 isEstablished = true;
164 callback.established();
165 } catch (IOException e) {
166 callback.failed();
167 }
168 }).start();
169
170 }
171
172 public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
173 new Thread(() -> {
174 InputStream fileInputStream = null;
175 final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId());
176 try {
177 wakeLock.acquire();
178 MessageDigest digest = MessageDigest.getInstance("SHA-1");
179 digest.reset();
180 fileInputStream = connection.getFileInputStream();
181 if (fileInputStream == null) {
182 Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create input stream");
183 callback.onFileTransferAborted();
184 return;
185 }
186 final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
187 long size = file.getExpectedSize();
188 long transmitted = 0;
189 int count;
190 byte[] buffer = new byte[8192];
191 while ((count = innerInputStream.read(buffer)) > 0) {
192 outputStream.write(buffer, 0, count);
193 digest.update(buffer, 0, count);
194 transmitted += count;
195 connection.updateProgress((int) ((((double) transmitted) / size) * 100));
196 }
197 outputStream.flush();
198 file.setSha1Sum(digest.digest());
199 if (callback != null) {
200 callback.onFileTransmitted(file);
201 }
202 } catch (Exception e) {
203 Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage());
204 callback.onFileTransferAborted();
205 } finally {
206 FileBackend.close(fileInputStream);
207 WakeLockHelper.release(wakeLock);
208 }
209 }).start();
210
211 }
212
213 public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
214 new Thread(() -> {
215 OutputStream fileOutputStream = null;
216 final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getSessionId());
217 try {
218 wakeLock.acquire();
219 MessageDigest digest = MessageDigest.getInstance("SHA-1");
220 digest.reset();
221 //inputStream.skip(45);
222 socket.setSoTimeout(30000);
223 fileOutputStream = connection.getFileOutputStream();
224 if (fileOutputStream == null) {
225 callback.onFileTransferAborted();
226 Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create output stream");
227 return;
228 }
229 double size = file.getExpectedSize();
230 long remainingSize = file.getExpectedSize();
231 byte[] buffer = new byte[8192];
232 int count;
233 while (remainingSize > 0) {
234 count = inputStream.read(buffer);
235 if (count == -1) {
236 callback.onFileTransferAborted();
237 Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining");
238 return;
239 } else {
240 fileOutputStream.write(buffer, 0, count);
241 digest.update(buffer, 0, count);
242 remainingSize -= count;
243 }
244 connection.updateProgress((int) (((size - remainingSize) / size) * 100));
245 }
246 fileOutputStream.flush();
247 fileOutputStream.close();
248 file.setSha1Sum(digest.digest());
249 callback.onFileTransmitted(file);
250 } catch (Exception e) {
251 Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage());
252 callback.onFileTransferAborted();
253 } finally {
254 WakeLockHelper.release(wakeLock);
255 FileBackend.close(fileOutputStream);
256 FileBackend.close(inputStream);
257 }
258 }).start();
259 }
260
261 public boolean isProxy() {
262 return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
263 }
264
265 public boolean needsActivation() {
266 return (this.isProxy() && !this.activated);
267 }
268
269 public void disconnect() {
270 FileBackend.close(inputStream);
271 FileBackend.close(outputStream);
272 FileBackend.close(socket);
273 FileBackend.close(serverSocket);
274 }
275
276 public boolean isEstablished() {
277 return this.isEstablished;
278 }
279
280 public JingleCandidate getCandidate() {
281 return this.candidate;
282 }
283
284 public void setActivated(boolean activated) {
285 this.activated = activated;
286 }
287}