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