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 this.file.setExpectedSize(size);
463 Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
464 } else {
465 this.sendCancel();
466 this.fail();
467 }
468 } else {
469 this.sendCancel();
470 this.fail();
471 }
472 }
473
474 private void sendInitRequest() {
475 JinglePacket packet = this.bootstrapPacket("session-initiate");
476 Content content = new Content(this.contentCreator, this.contentName);
477 if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
478 content.setTransportId(this.transportId);
479 this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
480 Pair<InputStream,Integer> pair;
481 try {
482 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
483 Conversation conversation = this.message.getConversation();
484 if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
485 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
486 cancel();
487 }
488 this.file.setKeyAndIv(conversation.getSymmetricKey());
489 pair = AbstractConnectionManager.createInputStream(this.file, false);
490 this.file.setExpectedSize(pair.second);
491 content.setFileOffer(this.file, true, this.ftVersion);
492 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
493 this.file.setKey(mXmppAxolotlMessage.getInnerKey());
494 this.file.setIv(mXmppAxolotlMessage.getIV());
495 pair = AbstractConnectionManager.createInputStream(this.file, true);
496 this.file.setExpectedSize(pair.second);
497 content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
498 } else {
499 pair = AbstractConnectionManager.createInputStream(this.file, false);
500 this.file.setExpectedSize(pair.second);
501 content.setFileOffer(this.file, false, this.ftVersion);
502 }
503 } catch (FileNotFoundException e) {
504 cancel();
505 return;
506 }
507 this.mFileInputStream = pair.first;
508 content.setTransportId(this.transportId);
509 content.socks5transport().setChildren(getCandidatesAsElements());
510 packet.setContent(content);
511 this.sendJinglePacket(packet,new OnIqPacketReceived() {
512
513 @Override
514 public void onIqPacketReceived(Account account, IqPacket packet) {
515 if (packet.getType() == IqPacket.TYPE.RESULT) {
516 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
517 mJingleStatus = JINGLE_STATUS_INITIATED;
518 mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
519 } else {
520 fail(IqParser.extractErrorMessage(packet));
521 }
522 }
523 });
524
525 }
526 }
527
528 private void sendHash() {
529 JinglePacket packet = this.bootstrapPacket("session-info");
530 packet.addChecksum(file.getSha1Sum(),ftVersion.getNamespace());
531 this.sendJinglePacket(packet);
532 }
533
534 private List<Element> getCandidatesAsElements() {
535 List<Element> elements = new ArrayList<>();
536 for (JingleCandidate c : this.candidates) {
537 if (c.isOurs()) {
538 elements.add(c.toElement());
539 }
540 }
541 return elements;
542 }
543
544 private void sendAccept() {
545 mJingleStatus = JINGLE_STATUS_ACCEPTED;
546 this.mStatus = Transferable.STATUS_DOWNLOADING;
547 this.mJingleConnectionManager.updateConversationUi(true);
548 this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
549 @Override
550 public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
551 final JinglePacket packet = bootstrapPacket("session-accept");
552 final Content content = new Content(contentCreator,contentName);
553 content.setFileOffer(fileOffer, ftVersion);
554 content.setTransportId(transportId);
555 if (success && candidate != null && !equalCandidateExists(candidate)) {
556 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
557 JingleConnection.this,
558 candidate);
559 connections.put(candidate.getCid(), socksConnection);
560 socksConnection.connect(new OnTransportConnected() {
561
562 @Override
563 public void failed() {
564 Log.d(Config.LOGTAG,"connection to our own primary candidate failed");
565 content.socks5transport().setChildren(getCandidatesAsElements());
566 packet.setContent(content);
567 sendJinglePacket(packet);
568 connectNextCandidate();
569 }
570
571 @Override
572 public void established() {
573 Log.d(Config.LOGTAG, "connected to primary candidate");
574 mergeCandidate(candidate);
575 content.socks5transport().setChildren(getCandidatesAsElements());
576 packet.setContent(content);
577 sendJinglePacket(packet);
578 connectNextCandidate();
579 }
580 });
581 } else {
582 Log.d(Config.LOGTAG,"did not find a primary candidate for ourself");
583 content.socks5transport().setChildren(getCandidatesAsElements());
584 packet.setContent(content);
585 sendJinglePacket(packet);
586 connectNextCandidate();
587 }
588 }
589 });
590 }
591
592 private JinglePacket bootstrapPacket(String action) {
593 JinglePacket packet = new JinglePacket();
594 packet.setAction(action);
595 packet.setFrom(account.getJid());
596 packet.setTo(this.message.getCounterpart());
597 packet.setSessionId(this.sessionId);
598 packet.setInitiator(this.initiator);
599 return packet;
600 }
601
602 private void sendJinglePacket(JinglePacket packet) {
603 mXmppConnectionService.sendIqPacket(account,packet,responseListener);
604 }
605
606 private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
607 mXmppConnectionService.sendIqPacket(account,packet,callback);
608 }
609
610 private boolean receiveAccept(JinglePacket packet) {
611 Content content = packet.getJingleContent();
612 mergeCandidates(JingleCandidate.parse(content.socks5transport()
613 .getChildren()));
614 this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
615 mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
616 this.connectNextCandidate();
617 return true;
618 }
619
620 private boolean receiveTransportInfo(JinglePacket packet) {
621 Content content = packet.getJingleContent();
622 if (content.hasSocks5Transport()) {
623 if (content.socks5transport().hasChild("activated")) {
624 if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
625 onProxyActivated.success();
626 } else {
627 String cid = content.socks5transport().findChild("activated").getAttribute("cid");
628 Log.d(Config.LOGTAG, "received proxy activated (" + cid
629 + ")prior to choosing our own transport");
630 JingleSocks5Transport connection = this.connections.get(cid);
631 if (connection != null) {
632 connection.setActivated(true);
633 } else {
634 Log.d(Config.LOGTAG, "activated connection not found");
635 this.sendCancel();
636 this.fail();
637 }
638 }
639 return true;
640 } else if (content.socks5transport().hasChild("proxy-error")) {
641 onProxyActivated.failed();
642 return true;
643 } else if (content.socks5transport().hasChild("candidate-error")) {
644 Log.d(Config.LOGTAG, "received candidate error");
645 this.receivedCandidate = true;
646 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
647 && (this.sentCandidate)) {
648 this.connect();
649 }
650 return true;
651 } else if (content.socks5transport().hasChild("candidate-used")) {
652 String cid = content.socks5transport()
653 .findChild("candidate-used").getAttribute("cid");
654 if (cid != null) {
655 Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
656 JingleCandidate candidate = getCandidate(cid);
657 if (candidate == null) {
658 Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
659 return false;
660 }
661 candidate.flagAsUsedByCounterpart();
662 this.receivedCandidate = true;
663 if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
664 && (this.sentCandidate)) {
665 this.connect();
666 } else {
667 Log.d(Config.LOGTAG,
668 "ignoring because file is already in transmission or we haven't sent our candidate yet");
669 }
670 return true;
671 } else {
672 return false;
673 }
674 } else {
675 return false;
676 }
677 } else {
678 return true;
679 }
680 }
681
682 private void connect() {
683 final JingleSocks5Transport connection = chooseConnection();
684 this.transport = connection;
685 if (connection == null) {
686 Log.d(Config.LOGTAG, "could not find suitable candidate");
687 this.disconnectSocks5Connections();
688 if (initiating()) {
689 this.sendFallbackToIbb();
690 }
691 } else {
692 this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
693 if (connection.needsActivation()) {
694 if (connection.getCandidate().isOurs()) {
695 final String sid;
696 if (ftVersion == Content.Version.FT_3) {
697 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": use session ID instead of transport ID to activate proxy");
698 sid = getSessionId();
699 } else {
700 sid = getTransportId();
701 }
702 Log.d(Config.LOGTAG, "candidate "
703 + connection.getCandidate().getCid()
704 + " was our proxy. going to activate");
705 IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
706 activation.setTo(connection.getCandidate().getJid());
707 activation.query("http://jabber.org/protocol/bytestreams")
708 .setAttribute("sid", sid);
709 activation.query().addChild("activate")
710 .setContent(this.getCounterPart().toString());
711 mXmppConnectionService.sendIqPacket(account,activation,
712 new OnIqPacketReceived() {
713
714 @Override
715 public void onIqPacketReceived(Account account,
716 IqPacket packet) {
717 if (packet.getType() != IqPacket.TYPE.RESULT) {
718 onProxyActivated.failed();
719 } else {
720 onProxyActivated.success();
721 sendProxyActivated(connection.getCandidate().getCid());
722 }
723 }
724 });
725 } else {
726 Log.d(Config.LOGTAG,
727 "candidate "
728 + connection.getCandidate().getCid()
729 + " was a proxy. waiting for other party to activate");
730 }
731 } else {
732 if (initiating()) {
733 Log.d(Config.LOGTAG, "we were initiating. sending file");
734 connection.send(file, onFileTransmissionStatusChanged);
735 } else {
736 Log.d(Config.LOGTAG, "we were responding. receiving file");
737 connection.receive(file, onFileTransmissionStatusChanged);
738 }
739 }
740 }
741 }
742
743 private JingleSocks5Transport chooseConnection() {
744 JingleSocks5Transport connection = null;
745 for (Entry<String, JingleSocks5Transport> cursor : connections
746 .entrySet()) {
747 JingleSocks5Transport currentConnection = cursor.getValue();
748 // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
749 if (currentConnection.isEstablished()
750 && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
751 .getCandidate().isOurs()))) {
752 // Log.d(Config.LOGTAG,"is usable");
753 if (connection == null) {
754 connection = currentConnection;
755 } else {
756 if (connection.getCandidate().getPriority() < currentConnection
757 .getCandidate().getPriority()) {
758 connection = currentConnection;
759 } else if (connection.getCandidate().getPriority() == currentConnection
760 .getCandidate().getPriority()) {
761 // Log.d(Config.LOGTAG,"found two candidates with same priority");
762 if (initiating()) {
763 if (currentConnection.getCandidate().isOurs()) {
764 connection = currentConnection;
765 }
766 } else {
767 if (!currentConnection.getCandidate().isOurs()) {
768 connection = currentConnection;
769 }
770 }
771 }
772 }
773 }
774 }
775 return connection;
776 }
777
778 private void sendSuccess() {
779 JinglePacket packet = bootstrapPacket("session-terminate");
780 Reason reason = new Reason();
781 reason.addChild("success");
782 packet.setReason(reason);
783 this.sendJinglePacket(packet);
784 this.disconnectSocks5Connections();
785 this.mJingleStatus = JINGLE_STATUS_FINISHED;
786 this.message.setStatus(Message.STATUS_RECEIVED);
787 this.message.setTransferable(null);
788 this.mXmppConnectionService.updateMessage(message);
789 this.mJingleConnectionManager.finishConnection(this);
790 }
791
792 private void sendFallbackToIbb() {
793 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": sending fallback to ibb");
794 JinglePacket packet = this.bootstrapPacket("transport-replace");
795 Content content = new Content(this.contentCreator, this.contentName);
796 this.transportId = this.mJingleConnectionManager.nextRandomId();
797 content.setTransportId(this.transportId);
798 content.ibbTransport().setAttribute("block-size",
799 Integer.toString(this.ibbBlockSize));
800 packet.setContent(content);
801 this.sendJinglePacket(packet);
802 }
803
804 OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
805 @Override
806 public void failed() {
807 Log.d(Config.LOGTAG, "ibb open failed");
808 }
809
810 @Override
811 public void established() {
812 JingleConnection.this.transport.send(file, onFileTransmissionStatusChanged);
813 }
814 };
815
816 private boolean receiveFallbackToIbb(JinglePacket packet) {
817 Log.d(Config.LOGTAG, "receiving fallack to ibb");
818 String receivedBlockSize = packet.getJingleContent().ibbTransport()
819 .getAttribute("block-size");
820 if (receivedBlockSize != null) {
821 int bs = Integer.parseInt(receivedBlockSize);
822 if (bs > this.ibbBlockSize) {
823 this.ibbBlockSize = bs;
824 }
825 }
826 this.transportId = packet.getJingleContent().getTransportId();
827 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
828
829 JinglePacket answer = bootstrapPacket("transport-accept");
830 Content content = new Content("initiator", "a-file-offer");
831 content.setTransportId(this.transportId);
832 content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
833 answer.setContent(content);
834
835
836 if (initiating()) {
837 this.sendJinglePacket(answer, new OnIqPacketReceived() {
838 @Override
839 public void onIqPacketReceived(Account account, IqPacket packet) {
840 if (packet.getType() == IqPacket.TYPE.RESULT) {
841 Log.d(Config.LOGTAG, account.getJid().toBareJid() + " recipient ACKed our transport-accept. creating ibb");
842 transport.connect(onIbbTransportConnected);
843 }
844 }
845 });
846 } else {
847 this.transport.receive(file, onFileTransmissionStatusChanged);
848 this.sendJinglePacket(answer);
849 }
850 return true;
851 }
852
853 private boolean receiveTransportAccept(JinglePacket packet) {
854 if (packet.getJingleContent().hasIbbTransport()) {
855 String receivedBlockSize = packet.getJingleContent().ibbTransport()
856 .getAttribute("block-size");
857 if (receivedBlockSize != null) {
858 int bs = Integer.parseInt(receivedBlockSize);
859 if (bs > this.ibbBlockSize) {
860 this.ibbBlockSize = bs;
861 }
862 }
863 this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
864
865 //might be receive instead if we are not initiating
866 if (initiating()) {
867 this.transport.connect(onIbbTransportConnected);
868 } else {
869 this.transport.receive(file, onFileTransmissionStatusChanged);
870 }
871 return true;
872 } else {
873 return false;
874 }
875 }
876
877 private void receiveSuccess() {
878 if (initiating()) {
879 this.mJingleStatus = JINGLE_STATUS_FINISHED;
880 this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
881 this.disconnectSocks5Connections();
882 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
883 this.transport.disconnect();
884 }
885 this.message.setTransferable(null);
886 this.mJingleConnectionManager.finishConnection(this);
887 } else {
888 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received session-terminate/success while responding");
889 }
890 }
891
892 public void cancel() {
893 this.disconnectSocks5Connections();
894 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
895 this.transport.disconnect();
896 }
897 this.sendCancel();
898 this.mJingleConnectionManager.finishConnection(this);
899 if (responding()) {
900 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
901 if (this.file!=null) {
902 file.delete();
903 }
904 this.mJingleConnectionManager.updateConversationUi(true);
905 } else {
906 this.mXmppConnectionService.markMessage(this.message,
907 Message.STATUS_SEND_FAILED);
908 this.message.setTransferable(null);
909 }
910 }
911
912 private void fail() {
913 fail(null);
914 }
915
916 private void fail(String errorMessage) {
917 this.mJingleStatus = JINGLE_STATUS_FAILED;
918 this.disconnectSocks5Connections();
919 if (this.transport != null && this.transport instanceof JingleInbandTransport) {
920 this.transport.disconnect();
921 }
922 FileBackend.close(mFileInputStream);
923 FileBackend.close(mFileOutputStream);
924 if (this.message != null) {
925 if (responding()) {
926 this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
927 if (this.file!=null) {
928 file.delete();
929 }
930 this.mJingleConnectionManager.updateConversationUi(true);
931 } else {
932 this.mXmppConnectionService.markMessage(this.message,
933 Message.STATUS_SEND_FAILED,
934 errorMessage);
935 this.message.setTransferable(null);
936 }
937 }
938 this.mJingleConnectionManager.finishConnection(this);
939 }
940
941 private void sendCancel() {
942 JinglePacket packet = bootstrapPacket("session-terminate");
943 Reason reason = new Reason();
944 reason.addChild("cancel");
945 packet.setReason(reason);
946 this.sendJinglePacket(packet);
947 }
948
949 private void connectNextCandidate() {
950 for (JingleCandidate candidate : this.candidates) {
951 if ((!connections.containsKey(candidate.getCid()) && (!candidate
952 .isOurs()))) {
953 this.connectWithCandidate(candidate);
954 return;
955 }
956 }
957 this.sendCandidateError();
958 }
959
960 private void connectWithCandidate(final JingleCandidate candidate) {
961 final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
962 this, candidate);
963 connections.put(candidate.getCid(), socksConnection);
964 socksConnection.connect(new OnTransportConnected() {
965
966 @Override
967 public void failed() {
968 Log.d(Config.LOGTAG,
969 "connection failed with " + candidate.getHost() + ":"
970 + candidate.getPort());
971 connectNextCandidate();
972 }
973
974 @Override
975 public void established() {
976 Log.d(Config.LOGTAG,
977 "established connection with " + candidate.getHost()
978 + ":" + candidate.getPort());
979 sendCandidateUsed(candidate.getCid());
980 }
981 });
982 }
983
984 private void disconnectSocks5Connections() {
985 Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
986 .entrySet().iterator();
987 while (it.hasNext()) {
988 Entry<String, JingleSocks5Transport> pairs = it.next();
989 pairs.getValue().disconnect();
990 it.remove();
991 }
992 }
993
994 private void sendProxyActivated(String cid) {
995 JinglePacket packet = bootstrapPacket("transport-info");
996 Content content = new Content(this.contentCreator, this.contentName);
997 content.setTransportId(this.transportId);
998 content.socks5transport().addChild("activated")
999 .setAttribute("cid", cid);
1000 packet.setContent(content);
1001 this.sendJinglePacket(packet);
1002 }
1003
1004 private void sendCandidateUsed(final String cid) {
1005 JinglePacket packet = bootstrapPacket("transport-info");
1006 Content content = new Content(this.contentCreator, this.contentName);
1007 content.setTransportId(this.transportId);
1008 content.socks5transport().addChild("candidate-used")
1009 .setAttribute("cid", cid);
1010 packet.setContent(content);
1011 this.sentCandidate = true;
1012 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1013 connect();
1014 }
1015 this.sendJinglePacket(packet);
1016 }
1017
1018 private void sendCandidateError() {
1019 JinglePacket packet = bootstrapPacket("transport-info");
1020 Content content = new Content(this.contentCreator, this.contentName);
1021 content.setTransportId(this.transportId);
1022 content.socks5transport().addChild("candidate-error");
1023 packet.setContent(content);
1024 this.sentCandidate = true;
1025 if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1026 connect();
1027 }
1028 this.sendJinglePacket(packet);
1029 }
1030
1031 public int getJingleStatus() {
1032 return this.mJingleStatus;
1033 }
1034
1035 private boolean equalCandidateExists(JingleCandidate candidate) {
1036 for (JingleCandidate c : this.candidates) {
1037 if (c.equalValues(candidate)) {
1038 return true;
1039 }
1040 }
1041 return false;
1042 }
1043
1044 private void mergeCandidate(JingleCandidate candidate) {
1045 for (JingleCandidate c : this.candidates) {
1046 if (c.equals(candidate)) {
1047 return;
1048 }
1049 }
1050 this.candidates.add(candidate);
1051 }
1052
1053 private void mergeCandidates(List<JingleCandidate> candidates) {
1054 for (JingleCandidate c : candidates) {
1055 mergeCandidate(c);
1056 }
1057 }
1058
1059 private JingleCandidate getCandidate(String cid) {
1060 for (JingleCandidate c : this.candidates) {
1061 if (c.getCid().equals(cid)) {
1062 return c;
1063 }
1064 }
1065 return null;
1066 }
1067
1068 public void updateProgress(int i) {
1069 this.mProgress = i;
1070 mJingleConnectionManager.updateConversationUi(false);
1071 }
1072
1073 public String getTransportId() {
1074 return this.transportId;
1075 }
1076
1077 public Content.Version getFtVersion() {
1078 return this.ftVersion;
1079 }
1080
1081 interface OnProxyActivated {
1082 void success();
1083
1084 void failed();
1085 }
1086
1087 public boolean hasTransportId(String sid) {
1088 return sid.equals(this.transportId);
1089 }
1090
1091 public JingleTransport getTransport() {
1092 return this.transport;
1093 }
1094
1095 public boolean start() {
1096 if (account.getStatus() == Account.State.ONLINE) {
1097 if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1098 new Thread(new Runnable() {
1099
1100 @Override
1101 public void run() {
1102 sendAccept();
1103 }
1104 }).start();
1105 }
1106 return true;
1107 } else {
1108 return false;
1109 }
1110 }
1111
1112 @Override
1113 public int getStatus() {
1114 return this.mStatus;
1115 }
1116
1117 @Override
1118 public long getFileSize() {
1119 if (this.file != null) {
1120 return this.file.getExpectedSize();
1121 } else {
1122 return 0;
1123 }
1124 }
1125
1126 @Override
1127 public int getProgress() {
1128 return this.mProgress;
1129 }
1130
1131 public AbstractConnectionManager getConnectionManager() {
1132 return this.mJingleConnectionManager;
1133 }
1134}