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