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