1package eu.siacs.conversations.xmpp.jingle;
2
3import android.util.Base64;
4import android.util.Log;
5
6import java.io.FileInputStream;
7import java.io.FileNotFoundException;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collections;
14import java.util.Comparator;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map.Entry;
18import java.util.concurrent.ConcurrentHashMap;
19
20import eu.siacs.conversations.Config;
21import eu.siacs.conversations.crypto.axolotl.AxolotlService;
22import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
23import eu.siacs.conversations.entities.Account;
24import eu.siacs.conversations.entities.Conversation;
25import eu.siacs.conversations.entities.DownloadableFile;
26import eu.siacs.conversations.entities.Message;
27import eu.siacs.conversations.entities.Presence;
28import eu.siacs.conversations.entities.ServiceDiscoveryResult;
29import eu.siacs.conversations.entities.Transferable;
30import eu.siacs.conversations.entities.TransferablePlaceholder;
31import eu.siacs.conversations.parser.IqParser;
32import eu.siacs.conversations.persistance.FileBackend;
33import eu.siacs.conversations.services.AbstractConnectionManager;
34import eu.siacs.conversations.services.XmppConnectionService;
35import eu.siacs.conversations.utils.CryptoHelper;
36import eu.siacs.conversations.xml.Element;
37import eu.siacs.conversations.xml.Namespace;
38import eu.siacs.conversations.xmpp.OnIqPacketReceived;
39import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
40import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
41import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
42import eu.siacs.conversations.xmpp.stanzas.IqPacket;
43import rocks.xmpp.addr.Jid;
44
45public class JingleConnection implements Transferable {
46
47 private static final int JINGLE_STATUS_INITIATED = 0;
48 private static final int JINGLE_STATUS_ACCEPTED = 1;
49 private static final int JINGLE_STATUS_FINISHED = 4;
50 static final int JINGLE_STATUS_TRANSMITTING = 5;
51 private static final int JINGLE_STATUS_FAILED = 99;
52 private static final int JINGLE_STATUS_OFFERED = -1;
53 private JingleConnectionManager mJingleConnectionManager;
54 private XmppConnectionService mXmppConnectionService;
55 private Content.Version ftVersion = Content.Version.FT_3;
56
57 private int ibbBlockSize = 8192;
58
59 private int mJingleStatus = JINGLE_STATUS_OFFERED;
60 private int mStatus = Transferable.STATUS_UNKNOWN;
61 private Message message;
62 private String sessionId;
63 private Account account;
64 private Jid initiator;
65 private Jid responder;
66 private List<JingleCandidate> candidates = new ArrayList<>();
67 private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
68
69 private String transportId;
70 private Element fileOffer;
71 private DownloadableFile file = null;
72
73 private String contentName;
74 private String contentCreator;
75 private Transport initialTransport;
76
77 private int mProgress = 0;
78
79 private boolean receivedCandidate = false;
80 private boolean sentCandidate = false;
81
82 private boolean acceptedAutomatically = false;
83 private boolean cancelled = false;
84
85 private XmppAxolotlMessage mXmppAxolotlMessage;
86
87 private JingleTransport transport = null;
88
89 private OutputStream mFileOutputStream;
90 private InputStream mFileInputStream;
91
92 private OnIqPacketReceived responseListener = (account, packet) -> {
93 if (packet.getType() != IqPacket.TYPE.RESULT) {
94 fail(IqParser.extractErrorMessage(packet));
95 }
96 };
97 private byte[] expectedHash = new byte[0];
98 private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
99
100 @Override
101 public void onFileTransmitted(DownloadableFile file) {
102 if (responding()) {
103 if (expectedHash.length > 0 && !Arrays.equals(expectedHash, file.getSha1Sum())) {
104 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": hashes did not match");
105 }
106 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": file transmitted(). we are responding");
107 sendSuccess();
108 mXmppConnectionService.getFileBackend().updateFileParams(message);
109 mXmppConnectionService.databaseBackend.createMessage(message);
110 mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
111 if (acceptedAutomatically) {
112 message.markUnread();
113 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
114 account.getPgpDecryptionService().decrypt(message, true);
115 } else {
116 mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleConnection.this.mXmppConnectionService.getNotificationService().push(message));
117
118 }
119 Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
120 return;
121 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
122 account.getPgpDecryptionService().decrypt(message, true);
123 }
124 } else {
125 if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info
126 sendHash();
127 }
128 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
129 account.getPgpDecryptionService().decrypt(message, false);
130 }
131 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
132 file.delete();
133 }
134 }
135 Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
136 if (message.getEncryption() != Message.ENCRYPTION_PGP) {
137 mXmppConnectionService.getFileBackend().updateMediaScanner(file);
138 }
139 }
140
141 @Override
142 public void onFileTransferAborted() {
143 JingleConnection.this.sendCancel();
144 JingleConnection.this.fail();
145 }
146 };
147 private OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
148 @Override
149 public void failed() {
150 Log.d(Config.LOGTAG, "ibb open failed");
151 }
152
153 @Override
154 public void established() {
155 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ibb transport connected. sending file");
156 mJingleStatus = JINGLE_STATUS_TRANSMITTING;
157 JingleConnection.this.transport.send(file, onFileTransmissionStatusChanged);
158 }
159 };
160 private OnProxyActivated onProxyActivated = new OnProxyActivated() {
161
162 @Override
163 public void success() {
164 if (initiator.equals(account.getJid())) {
165 Log.d(Config.LOGTAG, "we were initiating. sending file");
166 transport.send(file, onFileTransmissionStatusChanged);
167 } else {
168 transport.receive(file, onFileTransmissionStatusChanged);
169 Log.d(Config.LOGTAG, "we were responding. receiving file");
170 }
171 }
172
173 @Override
174 public void failed() {
175 Log.d(Config.LOGTAG, "proxy activation failed");
176 }
177 };
178
179 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
180 this.mJingleConnectionManager = mJingleConnectionManager;
181 this.mXmppConnectionService = mJingleConnectionManager
182 .getXmppConnectionService();
183 }
184
185 private boolean responding() {
186 return responder != null && responder.equals(account.getJid());
187 }
188
189 private boolean initiating() {
190 return initiator.equals(account.getJid());
191 }
192
193 InputStream getFileInputStream() {
194 return this.mFileInputStream;
195 }
196
197 OutputStream getFileOutputStream() throws IOException {
198 if (this.file == null) {
199 Log.d(Config.LOGTAG, "file object was not assigned");
200 return null;
201 }
202 this.file.getParentFile().mkdirs();
203 this.file.createNewFile();
204 this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file);
205 return this.mFileOutputStream;
206 }
207
208 public String getSessionId() {
209 return this.sessionId;
210 }
211
212 public Account getAccount() {
213 return this.account;
214 }
215
216 public Jid getCounterPart() {
217 return this.message.getCounterpart();
218 }
219
220 public void deliverPacket(JinglePacket packet) {
221 boolean returnResult = true;
222 if (packet.isAction("session-terminate")) {
223 Reason reason = packet.getReason();
224 if (reason != null) {
225 if (reason.hasChild("cancel")) {
226 this.fail();
227 } else if (reason.hasChild("success")) {
228 this.receiveSuccess();
229 } else {
230 this.fail();
231 }
232 } else {
233 this.fail();
234 }
235 } else if (packet.isAction("session-accept")) {
236 returnResult = receiveAccept(packet);
237 } else if (packet.isAction("session-info")) {
238 Element checksum = packet.getChecksum();
239 Element file = checksum == null ? null : checksum.findChild("file");
240 Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
241 if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
242 try {
243 this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
244 } catch (Exception e) {
245 this.expectedHash = new byte[0];
246 }
247 }
248 } else if (packet.isAction("transport-info")) {
249 returnResult = receiveTransportInfo(packet);
250 } else if (packet.isAction("transport-replace")) {
251 if (packet.getJingleContent().hasIbbTransport()) {
252 returnResult = this.receiveFallbackToIbb(packet);
253 } else {
254 returnResult = false;
255 Log.d(Config.LOGTAG, "trying to fallback to something unknown"
256 + packet.toString());
257 }
258 } else if (packet.isAction("transport-accept")) {
259 returnResult = this.receiveTransportAccept(packet);
260 } else {
261 Log.d(Config.LOGTAG, "packet arrived in connection. action was "
262 + packet.getAction());
263 returnResult = false;
264 }
265 IqPacket response;
266 if (returnResult) {
267 response = packet.generateResponse(IqPacket.TYPE.RESULT);
268
269 } else {
270 response = packet.generateResponse(IqPacket.TYPE.ERROR);
271 }
272 mXmppConnectionService.sendIqPacket(account, response, null);
273 }
274
275 public void init(final Message message) {
276 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
277 Conversation conversation = (Conversation) message.getConversation();
278 conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
279 if (xmppAxolotlMessage != null) {
280 init(message, xmppAxolotlMessage);
281 } else {
282 fail();
283 }
284 });
285 } else {
286 init(message, null);
287 }
288 }
289
290 private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
291 this.mXmppAxolotlMessage = xmppAxolotlMessage;
292 this.contentCreator = "initiator";
293 this.contentName = this.mJingleConnectionManager.nextRandomId();
294 this.message = message;
295 this.account = message.getConversation().getAccount();
296 upgradeNamespace();
297 this.initialTransport = getRemoteFeatures().contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB;
298 this.message.setTransferable(this);
299 this.mStatus = Transferable.STATUS_UPLOADING;
300 this.initiator = this.account.getJid();
301 this.responder = this.message.getCounterpart();
302 this.sessionId = this.mJingleConnectionManager.nextRandomId();
303 this.transportId = this.mJingleConnectionManager.nextRandomId();
304 if (this.initialTransport == Transport.IBB) {
305 this.sendInitRequest();
306 } else if (this.candidates.size() > 0) {
307 this.sendInitRequest(); //TODO we will never get here? Can probably be removed
308 } else {
309 this.mJingleConnectionManager.getPrimaryCandidate(account, (success, candidate) -> {
310 if (success) {
311 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
312 connections.put(candidate.getCid(), socksConnection);
313 socksConnection.connect(new OnTransportConnected() {
314
315 @Override
316 public void failed() {
317 Log.d(Config.LOGTAG,
318 "connection to our own primary candidete failed");
319 sendInitRequest();
320 }
321
322 @Override
323 public void established() {
324 Log.d(Config.LOGTAG,
325 "successfully connected to our own primary candidate");
326 mergeCandidate(candidate);
327 sendInitRequest();
328 }
329 });
330 mergeCandidate(candidate);
331 } else {
332 Log.d(Config.LOGTAG, "no primary candidate of our own was found");
333 sendInitRequest();
334 }
335 });
336 }
337
338 }
339
340 private void upgradeNamespace() {
341 List<String> features = getRemoteFeatures();
342 if (features.contains(Content.Version.FT_5.getNamespace())) {
343 this.ftVersion = Content.Version.FT_5;
344 } else if (features.contains(Content.Version.FT_4.getNamespace())) {
345 this.ftVersion = Content.Version.FT_4;
346 }
347 }
348
349 private List<String> getRemoteFeatures() {
350 Jid jid = this.message.getCounterpart();
351 String resource = jid != null ? jid.getResource() : null;
352 if (resource != null) {
353 Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
354 ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
355 return result == null ? Collections.emptyList() : result.getFeatures();
356 } else {
357 return Collections.emptyList();
358 }
359 }
360
361 public void init(Account account, JinglePacket packet) {
362 this.mJingleStatus = JINGLE_STATUS_INITIATED;
363 Conversation conversation = this.mXmppConnectionService
364 .findOrCreateConversation(account,
365 packet.getFrom().asBareJid(), false, false);
366 this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
367 this.message.setStatus(Message.STATUS_RECEIVED);
368 this.mStatus = Transferable.STATUS_OFFER;
369 this.message.setTransferable(this);
370 final Jid from = packet.getFrom();
371 this.message.setCounterpart(from);
372 this.account = account;
373 this.initiator = packet.getFrom();
374 this.responder = this.account.getJid();
375 this.sessionId = packet.getSessionId();
376 Content content = packet.getJingleContent();
377 this.contentCreator = content.getAttribute("creator");
378 this.initialTransport = content.hasSocks5Transport() ? Transport.SOCKS : Transport.IBB;
379 this.contentName = content.getAttribute("name");
380 this.transportId = content.getTransportId();
381
382 mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
383
384 if (this.initialTransport == Transport.SOCKS) {
385 this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
386 } else if (this.initialTransport == Transport.IBB) {
387 final String receivedBlockSize = content.ibbTransport().getAttribute("block-size");
388 if (receivedBlockSize != null) {
389 try {
390 this.ibbBlockSize = Math.min(Integer.parseInt(receivedBlockSize), this.ibbBlockSize);
391 } catch (NumberFormatException e) {
392 Log.d(Config.LOGTAG, "number format exception " + e.getMessage());
393 this.sendCancel();
394 this.fail();
395 return;
396 }
397 } else {
398 Log.d(Config.LOGTAG, "received block size was null");
399 this.sendCancel();
400 this.fail();
401 return;
402 }
403 }
404 this.ftVersion = content.getVersion();
405 if (ftVersion == null) {
406 this.sendCancel();
407 this.fail();
408 return;
409 }
410 this.fileOffer = content.getFileOffer(this.ftVersion);
411
412 if (fileOffer != null) {
413 Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
414 if (encrypted != null) {
415 this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
416 }
417 Element fileSize = fileOffer.findChild("size");
418 final String path = fileOffer.findChildContent("name");
419 if (path != null) {
420 AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
421 if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
422 message.setType(Message.TYPE_IMAGE);
423 message.setRelativeFilePath(message.getUuid() + "." + extension.main);
424 } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
425 if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
426 message.setType(Message.TYPE_IMAGE);
427 message.setRelativeFilePath(message.getUuid() + "." + extension.secondary);
428 } else {
429 message.setType(Message.TYPE_FILE);
430 message.setRelativeFilePath(message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
431 }
432 message.setEncryption(Message.ENCRYPTION_PGP);
433 } else {
434 message.setType(Message.TYPE_FILE);
435 message.setRelativeFilePath(message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
436 }
437 long size = parseLong(fileSize, 0);
438 message.setBody(Long.toString(size));
439 conversation.add(message);
440 mJingleConnectionManager.updateConversationUi(true);
441 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
442 if (mXmppAxolotlMessage != null) {
443 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
444 if (transportMessage != null) {
445 message.setEncryption(Message.ENCRYPTION_AXOLOTL);
446 this.file.setKey(transportMessage.getKey());
447 this.file.setIv(transportMessage.getIv());
448 message.setFingerprint(transportMessage.getFingerprint());
449 } else {
450 Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
451 }
452 }
453 message.resetFileParams();
454 this.file.setExpectedSize(size);
455 if (mJingleConnectionManager.hasStoragePermission()
456 && size < this.mJingleConnectionManager.getAutoAcceptFileSize()
457 && mXmppConnectionService.isDataSaverDisabled()) {
458 Log.d(Config.LOGTAG, "auto accepting file from " + packet.getFrom());
459 this.acceptedAutomatically = true;
460 this.sendAccept();
461 } else {
462 message.markUnread();
463 Log.d(Config.LOGTAG,
464 "not auto accepting new file offer with size: "
465 + size
466 + " allowed size:"
467 + this.mJingleConnectionManager
468 .getAutoAcceptFileSize());
469 this.mXmppConnectionService.getNotificationService().push(message);
470 }
471 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
472 } else {
473 this.sendCancel();
474 this.fail();
475 }
476 } else {
477 this.sendCancel();
478 this.fail();
479 }
480 }
481
482 private static long parseLong(final Element element, final long l) {
483 final String input = element == null ? null : element.getContent();
484 if (input == null) {
485 return l;
486 }
487 try {
488 return Long.parseLong(input);
489 } catch (Exception e) {
490 return l;
491 }
492 }
493
494 private void sendInitRequest() {
495 JinglePacket packet = this.bootstrapPacket("session-initiate");
496 Content content = new Content(this.contentCreator, this.contentName);
497 if (message.isFileOrImage()) {
498 content.setTransportId(this.transportId);
499 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
500 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
501 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
502 this.file.setIv(mXmppAxolotlMessage.getIV());
503 this.file.setExpectedSize(file.getSize() + 16);
504 content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
505 } else {
506 this.file.setExpectedSize(file.getSize());
507 content.setFileOffer(this.file, false, this.ftVersion);
508 }
509 message.resetFileParams();
510 try {
511 this.mFileInputStream = new FileInputStream(file);
512 } catch (FileNotFoundException e) {
513 abort();
514 return;
515 }
516 content.setTransportId(this.transportId);
517 if (this.initialTransport == Transport.IBB) {
518 content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize));
519 } else {
520 content.socks5transport().setChildren(getCandidatesAsElements());
521 }
522 packet.setContent(content);
523 this.sendJinglePacket(packet, (account, response) -> {
524 if (response.getType() == IqPacket.TYPE.RESULT) {
525 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": other party received offer");
526 if (mJingleStatus == JINGLE_STATUS_OFFERED) {
527 mJingleStatus = JINGLE_STATUS_INITIATED;
528 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
529 } else {
530 Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
531 }
532 } else {
533 fail(IqParser.extractErrorMessage(response));
534 }
535 });
536
537 }
538 }
539
540 private void sendHash() {
541 JinglePacket packet = this.bootstrapPacket("session-info");
542 packet.addChecksum(file.getSha1Sum(), ftVersion.getNamespace());
543 this.sendJinglePacket(packet);
544 }
545
546 private List<Element> getCandidatesAsElements() {
547 List<Element> elements = new ArrayList<>();
548 for (JingleCandidate c : this.candidates) {
549 if (c.isOurs()) {
550 elements.add(c.toElement());
551 }
552 }
553 return elements;
554 }
555
556 private void sendAccept() {
557 mJingleStatus = JINGLE_STATUS_ACCEPTED;
558 this.mStatus = Transferable.STATUS_DOWNLOADING;
559 this.mJingleConnectionManager.updateConversationUi(true);
560 if (initialTransport == Transport.SOCKS) {
561 sendAcceptSocks();
562 } else {
563 sendAcceptIbb();
564 }
565 }
566
567 private void sendAcceptSocks() {
568 this.mJingleConnectionManager.getPrimaryCandidate(this.account, (success, candidate) -> {
569 final JinglePacket packet = bootstrapPacket("session-accept");
570 final Content content = new Content(contentCreator, contentName);
571 content.setFileOffer(fileOffer, ftVersion);
572 content.setTransportId(transportId);
573 if (success && candidate != null && !equalCandidateExists(candidate)) {
574 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
575 connections.put(candidate.getCid(), socksConnection);
576 socksConnection.connect(new OnTransportConnected() {
577
578 @Override
579 public void failed() {
580 Log.d(Config.LOGTAG, "connection to our own primary candidate failed");
581 content.socks5transport().setChildren(getCandidatesAsElements());
582 packet.setContent(content);
583 sendJinglePacket(packet);
584 connectNextCandidate();
585 }
586
587 @Override
588 public void established() {
589 Log.d(Config.LOGTAG, "connected to primary candidate");
590 mergeCandidate(candidate);
591 content.socks5transport().setChildren(getCandidatesAsElements());
592 packet.setContent(content);
593 sendJinglePacket(packet);
594 connectNextCandidate();
595 }
596 });
597 } else {
598 Log.d(Config.LOGTAG, "did not find a primary candidate for ourself");
599 content.socks5transport().setChildren(getCandidatesAsElements());
600 packet.setContent(content);
601 sendJinglePacket(packet);
602 connectNextCandidate();
603 }
604 });
605 }
606
607 private void sendAcceptIbb() {
608 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
609 final JinglePacket packet = bootstrapPacket("session-accept");
610 final Content content = new Content(contentCreator, contentName);
611 content.setFileOffer(fileOffer, ftVersion);
612 content.setTransportId(transportId);
613 content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
614 packet.setContent(content);
615 this.transport.receive(file, onFileTransmissionStatusChanged);
616 this.sendJinglePacket(packet);
617 }
618
619 private JinglePacket bootstrapPacket(String action) {
620 JinglePacket packet = new JinglePacket();
621 packet.setAction(action);
622 packet.setFrom(account.getJid());
623 packet.setTo(this.message.getCounterpart());
624 packet.setSessionId(this.sessionId);
625 packet.setInitiator(this.initiator);
626 return packet;
627 }
628
629 private void sendJinglePacket(JinglePacket packet) {
630 mXmppConnectionService.sendIqPacket(account, packet, responseListener);
631 }
632
633 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
634 mXmppConnectionService.sendIqPacket(account, packet, callback);
635 }
636
637 private boolean receiveAccept(JinglePacket packet) {
638 if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
639 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept");
640 return false;
641 }
642 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
643 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
644 Content content = packet.getJingleContent();
645 if (content.hasSocks5Transport()) {
646 mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
647 this.connectNextCandidate();
648 return true;
649 } else if (content.hasIbbTransport()) {
650 String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
651 if (receivedBlockSize != null) {
652 try {
653 int bs = Integer.parseInt(receivedBlockSize);
654 if (bs > this.ibbBlockSize) {
655 this.ibbBlockSize = bs;
656 }
657 } catch (Exception e) {
658 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept");
659 }
660 }
661 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
662 this.transport.connect(onIbbTransportConnected);
663 return true;
664 } else {
665 return false;
666 }
667 }
668
669 private boolean receiveTransportInfo(JinglePacket packet) {
670 Content content = packet.getJingleContent();
671 if (content.hasSocks5Transport()) {
672 if (content.socks5transport().hasChild("activated")) {
673 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
674 onProxyActivated.success();
675 } else {
676 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
677 Log.d(Config.LOGTAG, "received proxy activated (" + cid
678 + ")prior to choosing our own transport");
679 JingleSocks5Transport connection = this.connections.get(cid);
680 if (connection != null) {
681 connection.setActivated(true);
682 } else {
683 Log.d(Config.LOGTAG, "activated connection not found");
684 this.sendCancel();
685 this.fail();
686 }
687 }
688 return true;
689 } else if (content.socks5transport().hasChild("proxy-error")) {
690 onProxyActivated.failed();
691 return true;
692 } else if (content.socks5transport().hasChild("candidate-error")) {
693 Log.d(Config.LOGTAG, "received candidate error");
694 this.receivedCandidate = true;
695 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
696 this.connect();
697 }
698 return true;
699 } else if (content.socks5transport().hasChild("candidate-used")) {
700 String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid");
701 if (cid != null) {
702 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
703 JingleCandidate candidate = getCandidate(cid);
704 if (candidate == null) {
705 Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
706 return false;
707 }
708 candidate.flagAsUsedByCounterpart();
709 this.receivedCandidate = true;
710 if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
711 this.connect();
712 } else {
713 Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
714 }
715 return true;
716 } else {
717 return false;
718 }
719 } else {
720 return false;
721 }
722 } else {
723 return true;
724 }
725 }
726
727 private void connect() {
728 final JingleSocks5Transport connection = chooseConnection();
729 this.transport = connection;
730 if (connection == null) {
731 Log.d(Config.LOGTAG, "could not find suitable candidate");
732 this.disconnectSocks5Connections();
733 if (initiating()) {
734 this.sendFallbackToIbb();
735 }
736 } else {
737 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
738 if (connection.needsActivation()) {
739 if (connection.getCandidate().isOurs()) {
740 final String sid;
741 if (ftVersion == Content.Version.FT_3) {
742 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
743 sid = getSessionId();
744 } else {
745 sid = getTransportId();
746 }
747 Log.d(Config.LOGTAG, "candidate "
748 + connection.getCandidate().getCid()
749 + " was our proxy. going to activate");
750 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
751 activation.setTo(connection.getCandidate().getJid());
752 activation.query("http://jabber.org/protocol/bytestreams")
753 .setAttribute("sid", sid);
754 activation.query().addChild("activate")
755 .setContent(this.getCounterPart().toString());
756 mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> {
757 if (response.getType() != IqPacket.TYPE.RESULT) {
758 onProxyActivated.failed();
759 } else {
760 onProxyActivated.success();
761 sendProxyActivated(connection.getCandidate().getCid());
762 }
763 });
764 } else {
765 Log.d(Config.LOGTAG,
766 "candidate "
767 + connection.getCandidate().getCid()
768 + " was a proxy. waiting for other party to activate");
769 }
770 } else {
771 if (initiating()) {
772 Log.d(Config.LOGTAG, "we were initiating. sending file");
773 connection.send(file, onFileTransmissionStatusChanged);
774 } else {
775 Log.d(Config.LOGTAG, "we were responding. receiving file");
776 connection.receive(file, onFileTransmissionStatusChanged);
777 }
778 }
779 }
780 }
781
782 private JingleSocks5Transport chooseConnection() {
783 JingleSocks5Transport connection = null;
784 for (Entry<String, JingleSocks5Transport> cursor : connections
785 .entrySet()) {
786 JingleSocks5Transport currentConnection = cursor.getValue();
787 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
788 if (currentConnection.isEstablished()
789 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
790 .getCandidate().isOurs()))) {
791 // Log.d(Config.LOGTAG,"is usable");
792 if (connection == null) {
793 connection = currentConnection;
794 } else {
795 if (connection.getCandidate().getPriority() < currentConnection
796 .getCandidate().getPriority()) {
797 connection = currentConnection;
798 } else if (connection.getCandidate().getPriority() == currentConnection
799 .getCandidate().getPriority()) {
800 // Log.d(Config.LOGTAG,"found two candidates with same priority");
801 if (initiating()) {
802 if (currentConnection.getCandidate().isOurs()) {
803 connection = currentConnection;
804 }
805 } else {
806 if (!currentConnection.getCandidate().isOurs()) {
807 connection = currentConnection;
808 }
809 }
810 }
811 }
812 }
813 }
814 return connection;
815 }
816
817 private void sendSuccess() {
818 JinglePacket packet = bootstrapPacket("session-terminate");
819 Reason reason = new Reason();
820 reason.addChild("success");
821 packet.setReason(reason);
822 this.sendJinglePacket(packet);
823 this.disconnectSocks5Connections();
824 this.mJingleStatus = JINGLE_STATUS_FINISHED;
825 this.message.setStatus(Message.STATUS_RECEIVED);
826 this.message.setTransferable(null);
827 this.mXmppConnectionService.updateMessage(message, false);
828 this.mJingleConnectionManager.finishConnection(this);
829 }
830
831 private void sendFallbackToIbb() {
832 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending fallback to ibb");
833 JinglePacket packet = this.bootstrapPacket("transport-replace");
834 Content content = new Content(this.contentCreator, this.contentName);
835 this.transportId = this.mJingleConnectionManager.nextRandomId();
836 content.setTransportId(this.transportId);
837 content.ibbTransport().setAttribute("block-size",
838 Integer.toString(this.ibbBlockSize));
839 packet.setContent(content);
840 this.sendJinglePacket(packet);
841 }
842
843
844 private boolean receiveFallbackToIbb(JinglePacket packet) {
845 Log.d(Config.LOGTAG, "receiving fallback to ibb");
846 final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
847 if (receivedBlockSize != null) {
848 try {
849 final int bs = Integer.parseInt(receivedBlockSize);
850 if (bs < this.ibbBlockSize) {
851 this.ibbBlockSize = bs;
852 }
853 } catch (NumberFormatException e) {
854 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
855 }
856 }
857 this.transportId = packet.getJingleContent().getTransportId();
858 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
859
860 final JinglePacket answer = bootstrapPacket("transport-accept");
861
862 final Content content = new Content(contentCreator, contentName);
863 content.ibbTransport().setAttribute("block-size", this.ibbBlockSize);
864 content.ibbTransport().setAttribute("sid", this.transportId);
865 answer.setContent(content);
866
867
868 if (initiating()) {
869 this.sendJinglePacket(answer, (account, response) -> {
870 if (response.getType() == IqPacket.TYPE.RESULT) {
871 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
872 transport.connect(onIbbTransportConnected);
873 }
874 });
875 } else {
876 this.transport.receive(file, onFileTransmissionStatusChanged);
877 this.sendJinglePacket(answer);
878 }
879 return true;
880 }
881
882 private boolean receiveTransportAccept(JinglePacket packet) {
883 if (packet.getJingleContent().hasIbbTransport()) {
884 String receivedBlockSize = packet.getJingleContent().ibbTransport()
885 .getAttribute("block-size");
886 if (receivedBlockSize != null) {
887 try {
888 int bs = Integer.parseInt(receivedBlockSize);
889 if (bs < this.ibbBlockSize) {
890 this.ibbBlockSize = bs;
891 }
892 } catch (NumberFormatException e) {
893 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-accept");
894 }
895 }
896 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
897
898 //might be receive instead if we are not initiating
899 if (initiating()) {
900 this.transport.connect(onIbbTransportConnected);
901 } else {
902 this.transport.receive(file, onFileTransmissionStatusChanged);
903 }
904 return true;
905 } else {
906 return false;
907 }
908 }
909
910 private void receiveSuccess() {
911 if (initiating()) {
912 this.mJingleStatus = JINGLE_STATUS_FINISHED;
913 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
914 this.disconnectSocks5Connections();
915 if (this.transport instanceof JingleInbandTransport) {
916 this.transport.disconnect();
917 }
918 this.message.setTransferable(null);
919 this.mJingleConnectionManager.finishConnection(this);
920 } else {
921 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received session-terminate/success while responding");
922 }
923 }
924
925 @Override
926 public void cancel() {
927 this.cancelled = true;
928 abort();
929 }
930
931 public void abort() {
932 this.disconnectSocks5Connections();
933 if (this.transport instanceof JingleInbandTransport) {
934 this.transport.disconnect();
935 }
936 this.sendCancel();
937 this.mJingleConnectionManager.finishConnection(this);
938 if (responding()) {
939 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
940 if (this.file != null) {
941 file.delete();
942 }
943 this.mJingleConnectionManager.updateConversationUi(true);
944 } else {
945 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
946 this.message.setTransferable(null);
947 }
948 }
949
950 private void fail() {
951 fail(null);
952 }
953
954 private void fail(String errorMessage) {
955 this.mJingleStatus = JINGLE_STATUS_FAILED;
956 this.disconnectSocks5Connections();
957 if (this.transport instanceof JingleInbandTransport) {
958 this.transport.disconnect();
959 }
960 FileBackend.close(mFileInputStream);
961 FileBackend.close(mFileOutputStream);
962 if (this.message != null) {
963 if (responding()) {
964 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
965 if (this.file != null) {
966 file.delete();
967 }
968 this.mJingleConnectionManager.updateConversationUi(true);
969 } else {
970 this.mXmppConnectionService.markMessage(this.message,
971 Message.STATUS_SEND_FAILED,
972 cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
973 this.message.setTransferable(null);
974 }
975 }
976 this.mJingleConnectionManager.finishConnection(this);
977 }
978
979 private void sendCancel() {
980 JinglePacket packet = bootstrapPacket("session-terminate");
981 Reason reason = new Reason();
982 reason.addChild("cancel");
983 packet.setReason(reason);
984 this.sendJinglePacket(packet);
985 }
986
987 private void connectNextCandidate() {
988 for (JingleCandidate candidate : this.candidates) {
989 if ((!connections.containsKey(candidate.getCid()) && (!candidate
990 .isOurs()))) {
991 this.connectWithCandidate(candidate);
992 return;
993 }
994 }
995 this.sendCandidateError();
996 }
997
998 private void connectWithCandidate(final JingleCandidate candidate) {
999 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
1000 this, candidate);
1001 connections.put(candidate.getCid(), socksConnection);
1002 socksConnection.connect(new OnTransportConnected() {
1003
1004 @Override
1005 public void failed() {
1006 Log.d(Config.LOGTAG,
1007 "connection failed with " + candidate.getHost() + ":"
1008 + candidate.getPort());
1009 connectNextCandidate();
1010 }
1011
1012 @Override
1013 public void established() {
1014 Log.d(Config.LOGTAG,
1015 "established connection with " + candidate.getHost()
1016 + ":" + candidate.getPort());
1017 sendCandidateUsed(candidate.getCid());
1018 }
1019 });
1020 }
1021
1022 private void disconnectSocks5Connections() {
1023 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
1024 .entrySet().iterator();
1025 while (it.hasNext()) {
1026 Entry<String, JingleSocks5Transport> pairs = it.next();
1027 pairs.getValue().disconnect();
1028 it.remove();
1029 }
1030 }
1031
1032 private void sendProxyActivated(String cid) {
1033 JinglePacket packet = bootstrapPacket("transport-info");
1034 Content content = new Content(this.contentCreator, this.contentName);
1035 content.setTransportId(this.transportId);
1036 content.socks5transport().addChild("activated").setAttribute("cid", cid);
1037 packet.setContent(content);
1038 this.sendJinglePacket(packet);
1039 }
1040
1041 private void sendCandidateUsed(final String cid) {
1042 JinglePacket packet = bootstrapPacket("transport-info");
1043 Content content = new Content(this.contentCreator, this.contentName);
1044 content.setTransportId(this.transportId);
1045 content.socks5transport().addChild("candidate-used").setAttribute("cid", cid);
1046 packet.setContent(content);
1047 this.sentCandidate = true;
1048 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1049 connect();
1050 }
1051 this.sendJinglePacket(packet);
1052 }
1053
1054 private void sendCandidateError() {
1055 Log.d(Config.LOGTAG, "sending candidate error");
1056 JinglePacket packet = bootstrapPacket("transport-info");
1057 Content content = new Content(this.contentCreator, this.contentName);
1058 content.setTransportId(this.transportId);
1059 content.socks5transport().addChild("candidate-error");
1060 packet.setContent(content);
1061 this.sentCandidate = true;
1062 this.sendJinglePacket(packet);
1063 if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
1064 connect();
1065 }
1066 }
1067
1068 public int getJingleStatus() {
1069 return this.mJingleStatus;
1070 }
1071
1072 private boolean equalCandidateExists(JingleCandidate candidate) {
1073 for (JingleCandidate c : this.candidates) {
1074 if (c.equalValues(candidate)) {
1075 return true;
1076 }
1077 }
1078 return false;
1079 }
1080
1081 private void mergeCandidate(JingleCandidate candidate) {
1082 for (JingleCandidate c : this.candidates) {
1083 if (c.equals(candidate)) {
1084 return;
1085 }
1086 }
1087 this.candidates.add(candidate);
1088 }
1089
1090 private void mergeCandidates(List<JingleCandidate> candidates) {
1091 Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
1092 for (JingleCandidate c : candidates) {
1093 mergeCandidate(c);
1094 }
1095 }
1096
1097 private JingleCandidate getCandidate(String cid) {
1098 for (JingleCandidate c : this.candidates) {
1099 if (c.getCid().equals(cid)) {
1100 return c;
1101 }
1102 }
1103 return null;
1104 }
1105
1106 void updateProgress(int i) {
1107 this.mProgress = i;
1108 mJingleConnectionManager.updateConversationUi(false);
1109 }
1110
1111 public String getTransportId() {
1112 return this.transportId;
1113 }
1114
1115 public Content.Version getFtVersion() {
1116 return this.ftVersion;
1117 }
1118
1119 public boolean hasTransportId(String sid) {
1120 return sid.equals(this.transportId);
1121 }
1122
1123 public JingleTransport getTransport() {
1124 return this.transport;
1125 }
1126
1127 public boolean start() {
1128 if (account.getStatus() == Account.State.ONLINE) {
1129 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1130 new Thread(this::sendAccept).start();
1131 }
1132 return true;
1133 } else {
1134 return false;
1135 }
1136 }
1137
1138 @Override
1139 public int getStatus() {
1140 return this.mStatus;
1141 }
1142
1143 @Override
1144 public long getFileSize() {
1145 if (this.file != null) {
1146 return this.file.getExpectedSize();
1147 } else {
1148 return 0;
1149 }
1150 }
1151
1152 @Override
1153 public int getProgress() {
1154 return this.mProgress;
1155 }
1156
1157 public AbstractConnectionManager getConnectionManager() {
1158 return this.mJingleConnectionManager;
1159 }
1160
1161 interface OnProxyActivated {
1162 void success();
1163
1164 void failed();
1165 }
1166}