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