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