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 protected static final int JINGLE_STATUS_INITIATED = 0;
49 protected static final int JINGLE_STATUS_ACCEPTED = 1;
50 protected static final int JINGLE_STATUS_FINISHED = 4;
51 protected static final int JINGLE_STATUS_TRANSMITTING = 5;
52 protected static final int JINGLE_STATUS_FAILED = 99;
53
54 private Content.Version ftVersion = Content.Version.FT_3;
55
56 private int ibbBlockSize = 8192;
57
58 private int mJingleStatus = -1;
59 private int mStatus = Transferable.STATUS_UNKNOWN;
60 private Message message;
61 private String sessionId;
62 private Account account;
63 private Jid initiator;
64 private Jid responder;
65 private List<JingleCandidate> candidates = new ArrayList<>();
66 private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
67
68 private String transportId;
69 private Element fileOffer;
70 private DownloadableFile file = null;
71
72 private String contentName;
73 private String contentCreator;
74
75 private int mProgress = 0;
76
77 private boolean receivedCandidate = false;
78 private boolean sentCandidate = false;
79
80 private boolean acceptedAutomatically = false;
81
82 private XmppAxolotlMessage mXmppAxolotlMessage;
83
84 private JingleTransport transport = null;
85
86 private OutputStream mFileOutputStream;
87 private InputStream mFileInputStream;
88
89 private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
90
91 @Override
92 public void onIqPacketReceived(Account account, IqPacket packet) {
93 if (packet.getType() != IqPacket.TYPE.RESULT) {
94 fail(IqParser.extractErrorMessage(packet));
95 }
96 }
97 };
98 private byte[] expectedHash = new byte[0];
99
100 private boolean responding() {
101 return responder.equals(account.getJid());
102 }
103
104 private boolean initiating() {
105 return initiator.equals(account.getJid());
106 }
107
108 final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
109
110 @Override
111 public void onFileTransmitted(DownloadableFile file) {
112 if (responding()) {
113 if (expectedHash.length > 0 && !Arrays.equals(expectedHash,file.getSha1Sum())) {
114 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": hashes did not match");
115 }
116 sendSuccess();
117 mXmppConnectionService.getFileBackend().updateFileParams(message);
118 mXmppConnectionService.databaseBackend.createMessage(message);
119 mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
120 if (acceptedAutomatically) {
121 message.markUnread();
122 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
123 account.getPgpDecryptionService().decrypt(message, true);
124 } else {
125 JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
126 }
127 }
128 } else {
129 if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info
130 sendHash();
131 }
132 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
133 account.getPgpDecryptionService().decrypt(message, false);
134 }
135 if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
136 file.delete();
137 }
138 }
139 Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+ CryptoHelper.bytesToHex(file.getSha1Sum())+")");
140 if (message.getEncryption() != Message.ENCRYPTION_PGP) {
141 mXmppConnectionService.getFileBackend().updateMediaScanner(file);
142 }
143 }
144
145 @Override
146 public void onFileTransferAborted() {
147 JingleConnection.this.sendCancel();
148 JingleConnection.this.fail();
149 }
150 };
151
152 public InputStream getFileInputStream() {
153 return this.mFileInputStream;
154 }
155
156 public OutputStream getFileOutputStream() {
157 return this.mFileOutputStream;
158 }
159
160 private OnProxyActivated onProxyActivated = new OnProxyActivated() {
161
162 @Override
163 public void success() {
164 if (initiator.equals(account.getJid())) {
165 Log.d(Config.LOGTAG, "we were initiating. sending file");
166 transport.send(file, onFileTransmissionStatusChanged);
167 } else {
168 transport.receive(file, onFileTransmissionStatusChanged);
169 Log.d(Config.LOGTAG, "we were responding. receiving file");
170 }
171 }
172
173 @Override
174 public void failed() {
175 Log.d(Config.LOGTAG, "proxy activation failed");
176 }
177 };
178
179 public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
180 this.mJingleConnectionManager = mJingleConnectionManager;
181 this.mXmppConnectionService = mJingleConnectionManager
182 .getXmppConnectionService();
183 }
184
185 public String getSessionId() {
186 return this.sessionId;
187 }
188
189 public Account getAccount() {
190 return this.account;
191 }
192
193 public Jid getCounterPart() {
194 return this.message.getCounterpart();
195 }
196
197 public void deliverPacket(JinglePacket packet) {
198 boolean returnResult = true;
199 if (packet.isAction("session-terminate")) {
200 Reason reason = packet.getReason();
201 if (reason != null) {
202 if (reason.hasChild("cancel")) {
203 this.fail();
204 } else if (reason.hasChild("success")) {
205 this.receiveSuccess();
206 } else {
207 this.fail();
208 }
209 } else {
210 this.fail();
211 }
212 } else if (packet.isAction("session-accept")) {
213 returnResult = receiveAccept(packet);
214 } else if (packet.isAction("session-info")) {
215 returnResult = true;
216 Element checksum = packet.getChecksum();
217 Element file = checksum == null ? null : checksum.findChild("file");
218 Element hash = file == null ? null : file.findChild("hash","urn:xmpp:hashes:2");
219 if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
220 try {
221 this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
222 } catch (Exception e) {
223 this.expectedHash = new byte[0];
224 }
225 }
226 } else if (packet.isAction("transport-info")) {
227 returnResult = receiveTransportInfo(packet);
228 } else if (packet.isAction("transport-replace")) {
229 if (packet.getJingleContent().hasIbbTransport()) {
230 returnResult = this.receiveFallbackToIbb(packet);
231 } else {
232 returnResult = false;
233 Log.d(Config.LOGTAG, "trying to fallback to something unknown"
234 + packet.toString());
235 }
236 } else if (packet.isAction("transport-accept")) {
237 returnResult = this.receiveTransportAccept(packet);
238 } else {
239 Log.d(Config.LOGTAG, "packet arrived in connection. action was "
240 + packet.getAction());
241 returnResult = false;
242 }
243 IqPacket response;
244 if (returnResult) {
245 response = packet.generateResponse(IqPacket.TYPE.RESULT);
246
247 } else {
248 response = packet.generateResponse(IqPacket.TYPE.ERROR);
249 }
250 mXmppConnectionService.sendIqPacket(account,response,null);
251 }
252
253 public void init(final Message message) {
254 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
255 Conversation conversation = message.getConversation();
256 conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
257 @Override
258 public void run(XmppAxolotlMessage xmppAxolotlMessage) {
259 if (xmppAxolotlMessage != null) {
260 init(message, xmppAxolotlMessage);
261 } else {
262 fail();
263 }
264 }
265 });
266 } else {
267 init(message, null);
268 }
269 }
270
271 private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
272 this.mXmppAxolotlMessage = xmppAxolotlMessage;
273 this.contentCreator = "initiator";
274 this.contentName = this.mJingleConnectionManager.nextRandomId();
275 this.message = message;
276 this.account = message.getConversation().getAccount();
277 upgradeNamespace();
278 this.message.setTransferable(this);
279 this.mStatus = Transferable.STATUS_UPLOADING;
280 this.initiator = this.account.getJid();
281 this.responder = this.message.getCounterpart();
282 this.sessionId = this.mJingleConnectionManager.nextRandomId();
283 this.transportId = this.mJingleConnectionManager.nextRandomId();
284 if (this.candidates.size() > 0) {
285 this.sendInitRequest();
286 } else {
287 this.mJingleConnectionManager.getPrimaryCandidate(account,
288 new OnPrimaryCandidateFound() {
289
290 @Override
291 public void onPrimaryCandidateFound(boolean success,
292 final JingleCandidate candidate) {
293 if (success) {
294 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
295 JingleConnection.this, candidate);
296 connections.put(candidate.getCid(),
297 socksConnection);
298 socksConnection
299 .connect(new OnTransportConnected() {
300
301 @Override
302 public void failed() {
303 Log.d(Config.LOGTAG,
304 "connection to our own primary candidete failed");
305 sendInitRequest();
306 }
307
308 @Override
309 public void established() {
310 Log.d(Config.LOGTAG,
311 "successfully connected to our own primary candidate");
312 mergeCandidate(candidate);
313 sendInitRequest();
314 }
315 });
316 mergeCandidate(candidate);
317 } else {
318 Log.d(Config.LOGTAG, "no primary candidate of our own was found");
319 sendInitRequest();
320 }
321 }
322 });
323 }
324
325 }
326
327 private void upgradeNamespace() {
328 Jid jid = this.message.getCounterpart();
329 String resource = jid != null ?jid.getResourcepart() : null;
330 if (resource != null) {
331 Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
332 ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
333 if (result != null) {
334 List<String> features = result.getFeatures();
335 if (features.contains(Content.Version.FT_5.getNamespace())) {
336 this.ftVersion = Content.Version.FT_5;
337 } else if (features.contains(Content.Version.FT_4.getNamespace())) {
338 this.ftVersion = Content.Version.FT_4;
339 }
340 }
341 }
342 }
343
344 public void init(Account account, JinglePacket packet) {
345 this.mJingleStatus = JINGLE_STATUS_INITIATED;
346 Conversation conversation = this.mXmppConnectionService
347 .findOrCreateConversation(account,
348 packet.getFrom().toBareJid(), false, false);
349 this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
350 this.message.setStatus(Message.STATUS_RECEIVED);
351 this.mStatus = Transferable.STATUS_OFFER;
352 this.message.setTransferable(this);
353 final Jid from = packet.getFrom();
354 this.message.setCounterpart(from);
355 this.account = account;
356 this.initiator = packet.getFrom();
357 this.responder = this.account.getJid();
358 this.sessionId = packet.getSessionId();
359 Content content = packet.getJingleContent();
360 this.contentCreator = content.getAttribute("creator");
361 this.contentName = content.getAttribute("name");
362 this.transportId = content.getTransportId();
363 this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
364 this.ftVersion = content.getVersion();
365 if (ftVersion == null) {
366 this.sendCancel();
367 this.fail();
368 return;
369 }
370 this.fileOffer = content.getFileOffer(this.ftVersion);
371
372 mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
373
374 if (fileOffer != null) {
375 Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
376 if (encrypted != null) {
377 this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
378 }
379 Element fileSize = fileOffer.findChild("size");
380 Element fileNameElement = fileOffer.findChild("name");
381 if (fileNameElement != null) {
382 String[] filename = fileNameElement.getContent()
383 .toLowerCase(Locale.US).toLowerCase().split("\\.");
384 String extension = filename[filename.length - 1];
385 if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
386 message.setType(Message.TYPE_IMAGE);
387 message.setRelativeFilePath(message.getUuid()+"."+extension);
388 } else if (VALID_CRYPTO_EXTENSIONS.contains(
389 filename[filename.length - 1])) {
390 if (filename.length == 3) {
391 extension = filename[filename.length - 2];
392 if (VALID_IMAGE_EXTENSIONS.contains(extension)) {
393 message.setType(Message.TYPE_IMAGE);
394 message.setRelativeFilePath(message.getUuid()+"."+extension);
395 } else {
396 message.setType(Message.TYPE_FILE);
397 }
398 if (filename[filename.length - 1].equals("otr")) {
399 message.setEncryption(Message.ENCRYPTION_OTR);
400 } else {
401 message.setEncryption(Message.ENCRYPTION_PGP);
402 }
403 }
404 } else {
405 message.setType(Message.TYPE_FILE);
406 }
407 if (message.getType() == Message.TYPE_FILE) {
408 String suffix = "";
409 if (!fileNameElement.getContent().isEmpty()) {
410 String parts[] = fileNameElement.getContent().split("/");
411 suffix = parts[parts.length - 1];
412 if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) {
413 suffix = suffix.substring(0,suffix.length() - 4);
414 } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) {
415 suffix = suffix.substring(0,suffix.length() - 4);
416 }
417 }
418 message.setRelativeFilePath(message.getUuid()+"_"+suffix);
419 }
420 long size = Long.parseLong(fileSize.getContent());
421 message.setBody(Long.toString(size));
422 conversation.add(message);
423 mJingleConnectionManager.updateConversationUi(true);
424 if (mJingleConnectionManager.hasStoragePermission()
425 && size < this.mJingleConnectionManager.getAutoAcceptFileSize()
426 && mXmppConnectionService.isDataSaverDisabled()) {
427 Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
428 this.acceptedAutomatically = true;
429 this.sendAccept();
430 } else {
431 message.markUnread();
432 Log.d(Config.LOGTAG,
433 "not auto accepting new file offer with size: "
434 + size
435 + " allowed size:"
436 + this.mJingleConnectionManager
437 .getAutoAcceptFileSize());
438 this.mXmppConnectionService.getNotificationService().push(message);
439 }
440 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
441 if (mXmppAxolotlMessage != null) {
442 XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
443 if (transportMessage != null) {
444 message.setEncryption(Message.ENCRYPTION_AXOLOTL);
445 this.file.setKey(transportMessage.getKey());
446 this.file.setIv(transportMessage.getIv());
447 message.setFingerprint(transportMessage.getFingerprint());
448 } else {
449 Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
450 }
451 } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
452 byte[] key = conversation.getSymmetricKey();
453 if (key == null) {
454 this.sendCancel();
455 this.fail();
456 return;
457 } else {
458 this.file.setKeyAndIv(key);
459 }
460 }
461 this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
462 if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
463 this.file.setExpectedSize((size / 16 + 1) * 16);
464 } else {
465 this.file.setExpectedSize(size);
466 }
467 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
468 } else {
469 this.sendCancel();
470 this.fail();
471 }
472 } else {
473 this.sendCancel();
474 this.fail();
475 }
476 }
477
478 private void sendInitRequest() {
479 JinglePacket packet = this.bootstrapPacket("session-initiate");
480 Content content = new Content(this.contentCreator, this.contentName);
481 if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
482 content.setTransportId(this.transportId);
483 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
484 Pair<InputStream,Integer> pair;
485 try {
486 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
487 Conversation conversation = this.message.getConversation();
488 if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
489 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
490 cancel();
491 }
492 this.file.setKeyAndIv(conversation.getSymmetricKey());
493 pair = AbstractConnectionManager.createInputStream(this.file, false);
494 this.file.setExpectedSize(pair.second);
495 content.setFileOffer(this.file, true, this.ftVersion);
496 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
497 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
498 this.file.setIv(mXmppAxolotlMessage.getIV());
499 pair = AbstractConnectionManager.createInputStream(this.file, true);
500 this.file.setExpectedSize(pair.second);
501 content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
502 } else {
503 pair = AbstractConnectionManager.createInputStream(this.file, false);
504 this.file.setExpectedSize(pair.second);
505 content.setFileOffer(this.file, false, this.ftVersion);
506 }
507 } catch (FileNotFoundException e) {
508 cancel();
509 return;
510 }
511 this.mFileInputStream = pair.first;
512 content.setTransportId(this.transportId);
513 content.socks5transport().setChildren(getCandidatesAsElements());
514 packet.setContent(content);
515 this.sendJinglePacket(packet,new OnIqPacketReceived() {
516
517 @Override
518 public void onIqPacketReceived(Account account, IqPacket packet) {
519 if (packet.getType() == IqPacket.TYPE.RESULT) {
520 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
521 mJingleStatus = JINGLE_STATUS_INITIATED;
522 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
523 } else {
524 fail(IqParser.extractErrorMessage(packet));
525 }
526 }
527 });
528
529 }
530 }
531
532 private void sendHash() {
533 JinglePacket packet = this.bootstrapPacket("session-info");
534 packet.addChecksum(file.getSha1Sum(),ftVersion.getNamespace());
535 this.sendJinglePacket(packet);
536 }
537
538 private List<Element> getCandidatesAsElements() {
539 List<Element> elements = new ArrayList<>();
540 for (JingleCandidate c : this.candidates) {
541 if (c.isOurs()) {
542 elements.add(c.toElement());
543 }
544 }
545 return elements;
546 }
547
548 private void sendAccept() {
549 mJingleStatus = JINGLE_STATUS_ACCEPTED;
550 this.mStatus = Transferable.STATUS_DOWNLOADING;
551 this.mJingleConnectionManager.updateConversationUi(true);
552 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
553 @Override
554 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
555 final JinglePacket packet = bootstrapPacket("session-accept");
556 final Content content = new Content(contentCreator,contentName);
557 content.setFileOffer(fileOffer, ftVersion);
558 content.setTransportId(transportId);
559 if (success && candidate != null && !equalCandidateExists(candidate)) {
560 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
561 JingleConnection.this,
562 candidate);
563 connections.put(candidate.getCid(), socksConnection);
564 socksConnection.connect(new OnTransportConnected() {
565
566 @Override
567 public void failed() {
568 Log.d(Config.LOGTAG,"connection to our own primary candidate failed");
569 content.socks5transport().setChildren(getCandidatesAsElements());
570 packet.setContent(content);
571 sendJinglePacket(packet);
572 connectNextCandidate();
573 }
574
575 @Override
576 public void established() {
577 Log.d(Config.LOGTAG, "connected to primary candidate");
578 mergeCandidate(candidate);
579 content.socks5transport().setChildren(getCandidatesAsElements());
580 packet.setContent(content);
581 sendJinglePacket(packet);
582 connectNextCandidate();
583 }
584 });
585 } else {
586 Log.d(Config.LOGTAG,"did not find a primary candidate for ourself");
587 content.socks5transport().setChildren(getCandidatesAsElements());
588 packet.setContent(content);
589 sendJinglePacket(packet);
590 connectNextCandidate();
591 }
592 }
593 });
594 }
595
596 private JinglePacket bootstrapPacket(String action) {
597 JinglePacket packet = new JinglePacket();
598 packet.setAction(action);
599 packet.setFrom(account.getJid());
600 packet.setTo(this.message.getCounterpart());
601 packet.setSessionId(this.sessionId);
602 packet.setInitiator(this.initiator);
603 return packet;
604 }
605
606 private void sendJinglePacket(JinglePacket packet) {
607 mXmppConnectionService.sendIqPacket(account,packet,responseListener);
608 }
609
610 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
611 mXmppConnectionService.sendIqPacket(account,packet,callback);
612 }
613
614 private boolean receiveAccept(JinglePacket packet) {
615 Content content = packet.getJingleContent();
616 mergeCandidates(JingleCandidate.parse(content.socks5transport()
617 .getChildren()));
618 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
619 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
620 this.connectNextCandidate();
621 return true;
622 }
623
624 private boolean receiveTransportInfo(JinglePacket packet) {
625 Content content = packet.getJingleContent();
626 if (content.hasSocks5Transport()) {
627 if (content.socks5transport().hasChild("activated")) {
628 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
629 onProxyActivated.success();
630 } else {
631 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
632 Log.d(Config.LOGTAG, "received proxy activated (" + cid
633 + ")prior to choosing our own transport");
634 JingleSocks5Transport connection = this.connections.get(cid);
635 if (connection != null) {
636 connection.setActivated(true);
637 } else {
638 Log.d(Config.LOGTAG, "activated connection not found");
639 this.sendCancel();
640 this.fail();
641 }
642 }
643 return true;
644 } else if (content.socks5transport().hasChild("proxy-error")) {
645 onProxyActivated.failed();
646 return true;
647 } else if (content.socks5transport().hasChild("candidate-error")) {
648 Log.d(Config.LOGTAG, "received candidate error");
649 this.receivedCandidate = true;
650 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
651 && (this.sentCandidate)) {
652 this.connect();
653 }
654 return true;
655 } else if (content.socks5transport().hasChild("candidate-used")) {
656 String cid = content.socks5transport()
657 .findChild("candidate-used").getAttribute("cid");
658 if (cid != null) {
659 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
660 JingleCandidate candidate = getCandidate(cid);
661 if (candidate == null) {
662 Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
663 return false;
664 }
665 candidate.flagAsUsedByCounterpart();
666 this.receivedCandidate = true;
667 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
668 && (this.sentCandidate)) {
669 this.connect();
670 } else {
671 Log.d(Config.LOGTAG,
672 "ignoring because file is already in transmission or we haven't sent our candidate yet");
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")
1013 .setAttribute("cid", cid);
1014 packet.setContent(content);
1015 this.sentCandidate = true;
1016 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1017 connect();
1018 }
1019 this.sendJinglePacket(packet);
1020 }
1021
1022 private void sendCandidateError() {
1023 JinglePacket packet = bootstrapPacket("transport-info");
1024 Content content = new Content(this.contentCreator, this.contentName);
1025 content.setTransportId(this.transportId);
1026 content.socks5transport().addChild("candidate-error");
1027 packet.setContent(content);
1028 this.sentCandidate = true;
1029 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1030 connect();
1031 }
1032 this.sendJinglePacket(packet);
1033 }
1034
1035 public int getJingleStatus() {
1036 return this.mJingleStatus;
1037 }
1038
1039 private boolean equalCandidateExists(JingleCandidate candidate) {
1040 for (JingleCandidate c : this.candidates) {
1041 if (c.equalValues(candidate)) {
1042 return true;
1043 }
1044 }
1045 return false;
1046 }
1047
1048 private void mergeCandidate(JingleCandidate candidate) {
1049 for (JingleCandidate c : this.candidates) {
1050 if (c.equals(candidate)) {
1051 return;
1052 }
1053 }
1054 this.candidates.add(candidate);
1055 }
1056
1057 private void mergeCandidates(List<JingleCandidate> candidates) {
1058 for (JingleCandidate c : candidates) {
1059 mergeCandidate(c);
1060 }
1061 }
1062
1063 private JingleCandidate getCandidate(String cid) {
1064 for (JingleCandidate c : this.candidates) {
1065 if (c.getCid().equals(cid)) {
1066 return c;
1067 }
1068 }
1069 return null;
1070 }
1071
1072 public void updateProgress(int i) {
1073 this.mProgress = i;
1074 mJingleConnectionManager.updateConversationUi(false);
1075 }
1076
1077 public String getTransportId() {
1078 return this.transportId;
1079 }
1080
1081 public Content.Version getFtVersion() {
1082 return this.ftVersion;
1083 }
1084
1085 interface OnProxyActivated {
1086 void success();
1087
1088 void failed();
1089 }
1090
1091 public boolean hasTransportId(String sid) {
1092 return sid.equals(this.transportId);
1093 }
1094
1095 public JingleTransport getTransport() {
1096 return this.transport;
1097 }
1098
1099 public boolean start() {
1100 if (account.getStatus() == Account.State.ONLINE) {
1101 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1102 new Thread(new Runnable() {
1103
1104 @Override
1105 public void run() {
1106 sendAccept();
1107 }
1108 }).start();
1109 }
1110 return true;
1111 } else {
1112 return false;
1113 }
1114 }
1115
1116 @Override
1117 public int getStatus() {
1118 return this.mStatus;
1119 }
1120
1121 @Override
1122 public long getFileSize() {
1123 if (this.file != null) {
1124 return this.file.getExpectedSize();
1125 } else {
1126 return 0;
1127 }
1128 }
1129
1130 @Override
1131 public int getProgress() {
1132 return this.mProgress;
1133 }
1134
1135 public AbstractConnectionManager getConnectionManager() {
1136 return this.mJingleConnectionManager;
1137 }
1138}