JingleConnection.java

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