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