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