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