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