JingleFileTransferConnection.java

   1package eu.siacs.conversations.xmpp.jingle;
   2
   3import android.util.Base64;
   4import android.util.Log;
   5
   6import com.google.common.base.Preconditions;
   7import com.google.common.base.Strings;
   8import com.google.common.collect.Collections2;
   9import com.google.common.collect.FluentIterable;
  10import com.google.common.collect.Iterables;
  11
  12import java.io.File;
  13import java.io.FileInputStream;
  14import java.io.FileNotFoundException;
  15import java.io.IOException;
  16import java.io.InputStream;
  17import java.io.OutputStream;
  18import java.util.ArrayList;
  19import java.util.Arrays;
  20import java.util.Collection;
  21import java.util.Collections;
  22import java.util.Iterator;
  23import java.util.List;
  24import java.util.Map.Entry;
  25import java.util.concurrent.ConcurrentHashMap;
  26
  27import eu.siacs.conversations.Config;
  28import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  29import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  30import eu.siacs.conversations.entities.Account;
  31import eu.siacs.conversations.entities.Conversation;
  32import eu.siacs.conversations.entities.DownloadableFile;
  33import eu.siacs.conversations.entities.Message;
  34import eu.siacs.conversations.entities.Presence;
  35import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  36import eu.siacs.conversations.entities.Transferable;
  37import eu.siacs.conversations.entities.TransferablePlaceholder;
  38import eu.siacs.conversations.parser.IqParser;
  39import eu.siacs.conversations.persistance.FileBackend;
  40import eu.siacs.conversations.services.AbstractConnectionManager;
  41import eu.siacs.conversations.utils.CryptoHelper;
  42import eu.siacs.conversations.xml.Element;
  43import eu.siacs.conversations.xml.Namespace;
  44import eu.siacs.conversations.xmpp.Jid;
  45import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  46import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
  47import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
  48import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
  49import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
  50import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  51import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
  52import eu.siacs.conversations.xmpp.jingle.stanzas.S5BTransportInfo;
  53import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  54
  55public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable {
  56
  57    private static final int JINGLE_STATUS_TRANSMITTING = 5;
  58    private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
  59    private static final int JINGLE_STATUS_INITIATED = 0;
  60    private static final int JINGLE_STATUS_ACCEPTED = 1;
  61    private static final int JINGLE_STATUS_FINISHED = 4;
  62    private static final int JINGLE_STATUS_FAILED = 99;
  63    private static final int JINGLE_STATUS_OFFERED = -1;
  64
  65    private static final int MAX_IBB_BLOCK_SIZE = 8192;
  66
  67    private int ibbBlockSize = MAX_IBB_BLOCK_SIZE;
  68
  69    private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum
  70    private int mStatus = Transferable.STATUS_UNKNOWN;
  71    private Message message;
  72    private Jid responder;
  73    private final List<JingleCandidate> candidates = new ArrayList<>();
  74    private final ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<>();
  75
  76    private String transportId;
  77    private FileTransferDescription description;
  78    private DownloadableFile file = null;
  79
  80    private boolean proxyActivationFailed = false;
  81
  82    private String contentName;
  83    private Content.Creator contentCreator;
  84    private Content.Senders contentSenders;
  85    private Class<? extends GenericTransportInfo> initialTransport;
  86    private boolean remoteSupportsOmemoJet;
  87
  88    private int mProgress = 0;
  89
  90    private boolean receivedCandidate = false;
  91    private boolean sentCandidate = false;
  92
  93    private boolean acceptedAutomatically = false;
  94    private boolean cancelled = false;
  95
  96    private XmppAxolotlMessage mXmppAxolotlMessage;
  97
  98    private JingleTransport transport = null;
  99
 100    private OutputStream mFileOutputStream;
 101    private InputStream mFileInputStream;
 102
 103    private final OnIqPacketReceived responseListener = (account, packet) -> {
 104        if (packet.getType() != IqPacket.TYPE.RESULT) {
 105            if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) {
 106                fail(IqParser.extractErrorMessage(packet));
 107            } else {
 108                Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString());
 109            }
 110        }
 111    };
 112    private byte[] expectedHash = new byte[0];
 113    private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
 114
 115        @Override
 116        public void onFileTransmitted(DownloadableFile file) {
 117            if (responding()) {
 118                if (expectedHash.length > 0) {
 119                    if (Arrays.equals(expectedHash, file.getSha1Sum())) {
 120                        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash");
 121                    } else {
 122                        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match");
 123                    }
 124                } else {
 125                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer");
 126                }
 127                sendSuccess();
 128                xmppConnectionService.getFileBackend().updateFileParams(message);
 129                xmppConnectionService.databaseBackend.createMessage(message);
 130                xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
 131                if (acceptedAutomatically) {
 132                    message.markUnread();
 133                    if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 134                        id.account.getPgpDecryptionService().decrypt(message, true);
 135                    } else {
 136                        xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message));
 137
 138                    }
 139                    Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
 140                    return;
 141                } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 142                    id.account.getPgpDecryptionService().decrypt(message, true);
 143                }
 144            } else {
 145                if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
 146                    sendHash();
 147                }
 148                if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 149                    id.account.getPgpDecryptionService().decrypt(message, false);
 150                }
 151                if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 152                    file.delete();
 153                }
 154                disconnectSocks5Connections();
 155            }
 156            Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
 157            if (message.getEncryption() != Message.ENCRYPTION_PGP) {
 158                xmppConnectionService.getFileBackend().updateMediaScanner(file);
 159            }
 160        }
 161
 162        @Override
 163        public void onFileTransferAborted() {
 164            JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
 165            JingleFileTransferConnection.this.fail();
 166        }
 167    };
 168    private final OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
 169        @Override
 170        public void failed() {
 171            Log.d(Config.LOGTAG, "ibb open failed");
 172        }
 173
 174        @Override
 175        public void established() {
 176            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file");
 177            mJingleStatus = JINGLE_STATUS_TRANSMITTING;
 178            JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged);
 179        }
 180    };
 181    private final OnProxyActivated onProxyActivated = new OnProxyActivated() {
 182
 183        @Override
 184        public void success() {
 185            if (isInitiator()) {
 186                Log.d(Config.LOGTAG, "we were initiating. sending file");
 187                transport.send(file, onFileTransmissionStatusChanged);
 188            } else {
 189                transport.receive(file, onFileTransmissionStatusChanged);
 190                Log.d(Config.LOGTAG, "we were responding. receiving file");
 191            }
 192        }
 193
 194        @Override
 195        public void failed() {
 196            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed");
 197            proxyActivationFailed = true;
 198            if (isInitiator()) {
 199                sendFallbackToIbb();
 200            }
 201        }
 202    };
 203
 204    JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
 205        super(jingleConnectionManager, id, initiator);
 206    }
 207
 208    private static long parseLong(final Element element, final long l) {
 209        final String input = element == null ? null : element.getContent();
 210        if (input == null) {
 211            return l;
 212        }
 213        try {
 214            return Long.parseLong(input);
 215        } catch (Exception e) {
 216            return l;
 217        }
 218    }
 219
 220    //TODO get rid and use isInitiator() instead
 221    private boolean responding() {
 222        return responder != null && responder.equals(id.account.getJid());
 223    }
 224
 225
 226    InputStream getFileInputStream() {
 227        return this.mFileInputStream;
 228    }
 229
 230    OutputStream getFileOutputStream() throws IOException {
 231        if (this.file == null) {
 232            Log.d(Config.LOGTAG, "file object was not assigned");
 233            return null;
 234        }
 235        final File parent = this.file.getParentFile();
 236        if (parent != null && parent.mkdirs()) {
 237            Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath());
 238        }
 239        if (this.file.createNewFile()) {
 240            Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath());
 241        }
 242        this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true);
 243        return this.mFileOutputStream;
 244    }
 245
 246    @Override
 247    void deliverPacket(final JinglePacket packet) {
 248        final JinglePacket.Action action = packet.getAction();
 249        //TODO switch case
 250        if (action == JinglePacket.Action.SESSION_INITIATE) {
 251            init(packet);
 252        } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
 253            final Reason reason = packet.getReason().reason;
 254            switch (reason) {
 255                case CANCEL:
 256                    this.cancelled = true;
 257                    this.fail();
 258                    break;
 259                case SUCCESS:
 260                    this.receiveSuccess();
 261                    break;
 262                default:
 263                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason);
 264                    this.fail();
 265                    break;
 266
 267            }
 268        } else if (action == JinglePacket.Action.SESSION_ACCEPT) {
 269            receiveAccept(packet);
 270        } else if (action == JinglePacket.Action.SESSION_INFO) {
 271            final Element checksum = packet.getJingleChild("checksum");
 272            final Element file = checksum == null ? null : checksum.findChild("file");
 273            final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
 274            if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
 275                try {
 276                    this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
 277                } catch (Exception e) {
 278                    this.expectedHash = new byte[0];
 279                }
 280            }
 281            respondToIq(packet, true);
 282        } else if (action == JinglePacket.Action.TRANSPORT_INFO) {
 283            receiveTransportInfo(packet);
 284        } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) {
 285            final Content content = packet.getJingleContent();
 286            final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
 287            if (transportInfo instanceof IbbTransportInfo) {
 288                receiveFallbackToIbb(packet, (IbbTransportInfo) transportInfo);
 289            } else {
 290                Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString());
 291                respondToIq(packet, false);
 292            }
 293        } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) {
 294            receiveTransportAccept(packet);
 295        } else {
 296            Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
 297            respondToIq(packet, false);
 298        }
 299    }
 300
 301    @Override
 302    void notifyRebound() {
 303        if (getJingleStatus() == JINGLE_STATUS_TRANSMITTING) {
 304            abort(Reason.CONNECTIVITY_ERROR);
 305        }
 306    }
 307
 308    private void respondToIq(final IqPacket packet, final boolean result) {
 309        final IqPacket response;
 310        if (result) {
 311            response = packet.generateResponse(IqPacket.TYPE.RESULT);
 312        } else {
 313            response = packet.generateResponse(IqPacket.TYPE.ERROR);
 314            final Element error = response.addChild("error").setAttribute("type", "cancel");
 315            error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
 316        }
 317        xmppConnectionService.sendIqPacket(id.account, response, null);
 318    }
 319
 320    private void respondToIqWithOutOfOrder(final IqPacket packet) {
 321        final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
 322        final Element error = response.addChild("error").setAttribute("type", "wait");
 323        error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas");
 324        error.addChild("out-of-order", "urn:xmpp:jingle:errors:1");
 325        xmppConnectionService.sendIqPacket(id.account, response, null);
 326    }
 327
 328    public void init(final Message message) {
 329        Preconditions.checkArgument(message.isFileOrImage());
 330        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 331            Conversation conversation = (Conversation) message.getConversation();
 332            conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
 333                if (xmppAxolotlMessage != null) {
 334                    init(message, xmppAxolotlMessage);
 335                } else {
 336                    fail();
 337                }
 338            });
 339        } else {
 340            init(message, null);
 341        }
 342    }
 343
 344    private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) {
 345        this.mXmppAxolotlMessage = xmppAxolotlMessage;
 346        this.contentCreator = Content.Creator.INITIATOR;
 347        this.contentSenders = Content.Senders.INITIATOR;
 348        this.contentName = JingleConnectionManager.nextRandomId();
 349        this.message = message;
 350        final List<String> remoteFeatures = getRemoteFeatures();
 351        final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures);
 352        this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? S5BTransportInfo.class : IbbTransportInfo.class;
 353        this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
 354        this.message.setTransferable(this);
 355        this.mStatus = Transferable.STATUS_UPLOADING;
 356        this.responder = this.id.with;
 357        this.transportId = JingleConnectionManager.nextRandomId();
 358        this.setupDescription(remoteVersion);
 359        if (this.initialTransport == IbbTransportInfo.class) {
 360            this.sendInitRequest();
 361        } else {
 362            gatherAndConnectDirectCandidates();
 363            this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> {
 364                if (success) {
 365                    final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
 366                    connections.put(candidate.getCid(), socksConnection);
 367                    socksConnection.connect(new OnTransportConnected() {
 368
 369                        @Override
 370                        public void failed() {
 371                            Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
 372                            sendInitRequest();
 373                        }
 374
 375                        @Override
 376                        public void established() {
 377                            Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
 378                            mergeCandidate(candidate);
 379                            sendInitRequest();
 380                        }
 381                    });
 382                    mergeCandidate(candidate);
 383                } else {
 384                    Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
 385                    sendInitRequest();
 386                }
 387            });
 388        }
 389
 390    }
 391
 392    private void gatherAndConnectDirectCandidates() {
 393        final List<JingleCandidate> directCandidates;
 394        if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
 395            if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) {
 396                directCandidates = Collections.emptyList();
 397            } else {
 398                directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid());
 399            }
 400        } else {
 401            directCandidates = Collections.emptyList();
 402        }
 403        for (JingleCandidate directCandidate : directCandidates) {
 404            final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
 405            connections.put(directCandidate.getCid(), socksConnection);
 406            candidates.add(directCandidate);
 407        }
 408    }
 409
 410    private FileTransferDescription.Version getAvailableFileTransferVersion(List<String> remoteFeatures) {
 411        if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) {
 412            return FileTransferDescription.Version.FT_5;
 413        } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) {
 414            return FileTransferDescription.Version.FT_4;
 415        } else {
 416            return FileTransferDescription.Version.FT_3;
 417        }
 418    }
 419
 420    private List<String> getRemoteFeatures() {
 421        final String resource = Strings.nullToEmpty(this.id.with.getResource());
 422        final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource);
 423        final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
 424        return result == null ? Collections.emptyList() : result.getFeatures();
 425    }
 426
 427    private void init(JinglePacket packet) { //should move to deliverPacket
 428        //TODO if not 'OFFERED' reply with out-of-order
 429        this.mJingleStatus = JINGLE_STATUS_INITIATED;
 430        final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false);
 431        this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
 432        this.message.setStatus(Message.STATUS_RECEIVED);
 433        this.mStatus = Transferable.STATUS_OFFER;
 434        this.message.setTransferable(this);
 435        this.message.setCounterpart(this.id.with);
 436        this.responder = this.id.account.getJid();
 437        final Content content = packet.getJingleContent();
 438        final GenericTransportInfo transportInfo = content.getTransport();
 439        this.contentCreator = content.getCreator();
 440        Content.Senders senders;
 441        try {
 442            senders = content.getSenders();
 443        } catch (final Exception e) {
 444            senders = Content.Senders.INITIATOR;
 445        }
 446        this.contentSenders = senders;
 447        this.contentName = content.getAttribute("name");
 448
 449        if (transportInfo instanceof S5BTransportInfo) {
 450            final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
 451            this.transportId = s5BTransportInfo.getTransportId();
 452            this.initialTransport = s5BTransportInfo.getClass();
 453            this.mergeCandidates(s5BTransportInfo.getCandidates());
 454        } else if (transportInfo instanceof IbbTransportInfo) {
 455            final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
 456            this.initialTransport = ibbTransportInfo.getClass();
 457            this.transportId = ibbTransportInfo.getTransportId();
 458            final int remoteBlockSize = ibbTransportInfo.getBlockSize();
 459            if (remoteBlockSize <= 0) {
 460                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party requested invalid ibb block size");
 461                respondToIq(packet, false);
 462                this.fail();
 463            }
 464            this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, ibbTransportInfo.getBlockSize());
 465        } else {
 466            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote tried to use unknown transport " + transportInfo.getNamespace());
 467            respondToIq(packet, false);
 468            this.fail();
 469            return;
 470        }
 471
 472        this.description = (FileTransferDescription) content.getDescription();
 473
 474        final Element fileOffer = this.description.getFileOffer();
 475
 476        if (fileOffer != null) {
 477            boolean remoteIsUsingJet = false;
 478            Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
 479            if (encrypted == null) {
 480                final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
 481                if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) {
 482                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET");
 483                    encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
 484                    remoteIsUsingJet = true;
 485                }
 486            }
 487            if (encrypted != null) {
 488                this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
 489            }
 490            Element fileSize = fileOffer.findChild("size");
 491            final String path = fileOffer.findChildContent("name");
 492            if (path != null) {
 493                AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
 494                if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
 495                    message.setType(Message.TYPE_IMAGE);
 496                    xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main);
 497                } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
 498                    if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
 499                        message.setType(Message.TYPE_IMAGE);
 500                        xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary);
 501                    } else {
 502                        message.setType(Message.TYPE_FILE);
 503                        xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
 504                    }
 505                    message.setEncryption(Message.ENCRYPTION_PGP);
 506                } else {
 507                    message.setType(Message.TYPE_FILE);
 508                    xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
 509                }
 510                long size = parseLong(fileSize, 0);
 511                Message.FileParams fp = new Message.FileParams();
 512                fp.size = new Long(size);
 513                message.setFileParams(fp);
 514                conversation.add(message);
 515                jingleConnectionManager.updateConversationUi(true);
 516                this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
 517                if (mXmppAxolotlMessage != null) {
 518                    XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
 519                    if (transportMessage != null) {
 520                        message.setEncryption(Message.ENCRYPTION_AXOLOTL);
 521                        this.file.setKey(transportMessage.getKey());
 522                        this.file.setIv(transportMessage.getIv());
 523                        message.setFingerprint(transportMessage.getFingerprint());
 524                    } else {
 525                        Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
 526                    }
 527                }
 528                //legacy OMEMO encrypted file transfers reported the file size after encryption
 529                //JET reports the plain text size. however lower levels of our receiving code still
 530                //expect the cipher text size. so we just + 16 bytes (auth tag size) here
 531                this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
 532
 533                respondToIq(packet, true);
 534
 535                if (id.account.getRoster().getContact(id.with).showInContactList()
 536                        && jingleConnectionManager.hasStoragePermission()
 537                        && size < this.jingleConnectionManager.getAutoAcceptFileSize()
 538                        && xmppConnectionService.isDataSaverDisabled()) {
 539                    Log.d(Config.LOGTAG, "auto accepting file from " + id.with);
 540                    this.acceptedAutomatically = true;
 541                    this.sendAccept();
 542                } else {
 543                    message.markUnread();
 544                    Log.d(Config.LOGTAG,
 545                            "not auto accepting new file offer with size: "
 546                                    + size
 547                                    + " allowed size:"
 548                                    + this.jingleConnectionManager
 549                                    .getAutoAcceptFileSize());
 550                    this.xmppConnectionService.getNotificationService().push(message);
 551                }
 552                Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
 553                return;
 554            }
 555            respondToIq(packet, false);
 556        }
 557    }
 558
 559    private void setupDescription(final FileTransferDescription.Version version) {
 560        this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
 561        final FileTransferDescription description;
 562        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 563            this.file.setKey(mXmppAxolotlMessage.getInnerKey());
 564            this.file.setIv(mXmppAxolotlMessage.getIV());
 565            //legacy OMEMO encrypted file transfer reported file size of the encrypted file
 566            //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
 567            this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
 568            if (remoteSupportsOmemoJet) {
 569                description = FileTransferDescription.of(this.file, version, null);
 570            } else {
 571                description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage);
 572            }
 573        } else {
 574            this.file.setExpectedSize(file.getSize());
 575            description = FileTransferDescription.of(this.file, version, null);
 576        }
 577        this.description = description;
 578    }
 579
 580    private void sendInitRequest() {
 581        final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
 582        final Content content = new Content(this.contentCreator, this.contentName);
 583        content.setSenders(this.contentSenders);
 584        if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
 585            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
 586            final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
 587            security.setAttribute("name", this.contentName);
 588            security.setAttribute("cipher", JET_OMEMO_CIPHER);
 589            security.setAttribute("type", AxolotlService.PEP_PREFIX);
 590            security.addChild(mXmppAxolotlMessage.toElement());
 591            content.addChild(security);
 592        }
 593        content.setDescription(this.description);
 594        try {
 595            this.mFileInputStream = new FileInputStream(file);
 596        } catch (FileNotFoundException e) {
 597            fail(e.getMessage());
 598            return;
 599        }
 600        if (this.initialTransport == IbbTransportInfo.class) {
 601            content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
 602            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
 603        } else {
 604            final Collection<JingleCandidate> candidates = getOurCandidates();
 605            content.setTransport(new S5BTransportInfo(this.transportId, candidates));
 606            Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
 607        }
 608        packet.addJingleContent(content);
 609        this.sendJinglePacket(packet, (account, response) -> {
 610            if (response.getType() == IqPacket.TYPE.RESULT) {
 611                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
 612                if (mJingleStatus == JINGLE_STATUS_OFFERED) {
 613                    mJingleStatus = JINGLE_STATUS_INITIATED;
 614                    xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
 615                } else {
 616                    Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
 617                }
 618            } else {
 619                fail(IqParser.extractErrorMessage(response));
 620            }
 621        });
 622
 623    }
 624
 625    private void sendHash() {
 626        final Element checksum = new Element("checksum", description.getVersion().getNamespace());
 627        checksum.setAttribute("creator", "initiator");
 628        checksum.setAttribute("name", "a-file-offer");
 629        Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2");
 630        hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP));
 631
 632        final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO);
 633        packet.addJingleChild(checksum);
 634        xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> {
 635            if (response.getType() == IqPacket.TYPE.ERROR) {
 636                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)");
 637            }
 638        });
 639    }
 640
 641    private Collection<JingleCandidate> getOurCandidates() {
 642        return Collections2.filter(this.candidates, c -> c != null && c.isOurs());
 643    }
 644
 645    private void sendAccept() {
 646        mJingleStatus = JINGLE_STATUS_ACCEPTED;
 647        this.mStatus = Transferable.STATUS_DOWNLOADING;
 648        this.jingleConnectionManager.updateConversationUi(true);
 649        if (initialTransport == S5BTransportInfo.class) {
 650            sendAcceptSocks();
 651        } else {
 652            sendAcceptIbb();
 653        }
 654    }
 655
 656    private void sendAcceptSocks() {
 657        gatherAndConnectDirectCandidates();
 658        this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
 659            final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
 660            final Content content = new Content(contentCreator, contentName);
 661            content.setSenders(this.contentSenders);
 662            content.setDescription(this.description);
 663            if (success && candidate != null && !equalCandidateExists(candidate)) {
 664                final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
 665                connections.put(candidate.getCid(), socksConnection);
 666                socksConnection.connect(new OnTransportConnected() {
 667
 668                    @Override
 669                    public void failed() {
 670                        Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
 671                        content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
 672                        packet.addJingleContent(content);
 673                        sendJinglePacket(packet);
 674                        connectNextCandidate();
 675                    }
 676
 677                    @Override
 678                    public void established() {
 679                        Log.d(Config.LOGTAG, "connected to proxy65 candidate");
 680                        mergeCandidate(candidate);
 681                        content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
 682                        packet.addJingleContent(content);
 683                        sendJinglePacket(packet);
 684                        connectNextCandidate();
 685                    }
 686                });
 687            } else {
 688                Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
 689                content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
 690                packet.addJingleContent(content);
 691                sendJinglePacket(packet);
 692                connectNextCandidate();
 693            }
 694        });
 695    }
 696
 697    private void sendAcceptIbb() {
 698        this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
 699        final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
 700        final Content content = new Content(contentCreator, contentName);
 701        content.setSenders(this.contentSenders);
 702        content.setDescription(this.description);
 703        content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
 704        packet.addJingleContent(content);
 705        this.transport.receive(file, onFileTransmissionStatusChanged);
 706        this.sendJinglePacket(packet);
 707    }
 708
 709    private JinglePacket bootstrapPacket(JinglePacket.Action action) {
 710        final JinglePacket packet = new JinglePacket(action, this.id.sessionId);
 711        packet.setTo(id.with);
 712        return packet;
 713    }
 714
 715    private void sendJinglePacket(JinglePacket packet) {
 716        xmppConnectionService.sendIqPacket(id.account, packet, responseListener);
 717    }
 718
 719    private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
 720        xmppConnectionService.sendIqPacket(id.account, packet, callback);
 721    }
 722
 723    private void receiveAccept(JinglePacket packet) {
 724        if (responding()) {
 725            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)");
 726            respondToIqWithOutOfOrder(packet);
 727            return;
 728        }
 729        if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
 730            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept");
 731            respondToIqWithOutOfOrder(packet);
 732            return;
 733        }
 734        this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
 735        xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
 736        final Content content = packet.getJingleContent();
 737        final GenericTransportInfo transportInfo = content.getTransport();
 738        //TODO we want to fail if transportInfo doesn’t match our intialTransport and/or our id
 739        if (transportInfo instanceof S5BTransportInfo) {
 740            final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
 741            respondToIq(packet, true);
 742            //TODO calling merge is probably a bug because that might eliminate candidates of the other party and lead to us not sending accept/deny
 743            //TODO: we probably just want to call add
 744            mergeCandidates(s5BTransportInfo.getCandidates());
 745            this.connectNextCandidate();
 746        } else if (transportInfo instanceof IbbTransportInfo) {
 747            final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
 748            final int remoteBlockSize = ibbTransportInfo.getBlockSize();
 749            if (remoteBlockSize > 0) {
 750                this.ibbBlockSize = Math.min(ibbBlockSize, remoteBlockSize);
 751            }
 752            respondToIq(packet, true);
 753            this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
 754            this.transport.connect(onIbbTransportConnected);
 755        } else {
 756            respondToIq(packet, false);
 757        }
 758    }
 759
 760    private void receiveTransportInfo(JinglePacket packet) {
 761        final Content content = packet.getJingleContent();
 762        final GenericTransportInfo transportInfo = content.getTransport();
 763        if (transportInfo instanceof S5BTransportInfo) {
 764            final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
 765            if (s5BTransportInfo.hasChild("activated")) {
 766                respondToIq(packet, true);
 767                if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
 768                    onProxyActivated.success();
 769                } else {
 770                    String cid = s5BTransportInfo.findChild("activated").getAttribute("cid");
 771                    Log.d(Config.LOGTAG, "received proxy activated (" + cid
 772                            + ")prior to choosing our own transport");
 773                    JingleSocks5Transport connection = this.connections.get(cid);
 774                    if (connection != null) {
 775                        connection.setActivated(true);
 776                    } else {
 777                        Log.d(Config.LOGTAG, "activated connection not found");
 778                        sendSessionTerminate(Reason.FAILED_TRANSPORT);
 779                        this.fail();
 780                    }
 781                }
 782            } else if (s5BTransportInfo.hasChild("proxy-error")) {
 783                respondToIq(packet, true);
 784                onProxyActivated.failed();
 785            } else if (s5BTransportInfo.hasChild("candidate-error")) {
 786                Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error");
 787                respondToIq(packet, true);
 788                this.receivedCandidate = true;
 789                if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
 790                    this.connect();
 791                }
 792            } else if (s5BTransportInfo.hasChild("candidate-used")) {
 793                String cid = s5BTransportInfo.findChild("candidate-used").getAttribute("cid");
 794                if (cid != null) {
 795                    Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
 796                    JingleCandidate candidate = getCandidate(cid);
 797                    if (candidate == null) {
 798                        Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
 799                        respondToIq(packet, false);
 800                        return;
 801                    }
 802                    respondToIq(packet, true);
 803                    candidate.flagAsUsedByCounterpart();
 804                    this.receivedCandidate = true;
 805                    if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
 806                        this.connect();
 807                    } else {
 808                        Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
 809                    }
 810                } else {
 811                    respondToIq(packet, false);
 812                }
 813            } else {
 814                respondToIq(packet, false);
 815            }
 816        } else {
 817            respondToIq(packet, true);
 818        }
 819    }
 820
 821    private void connect() {
 822        final JingleSocks5Transport connection = chooseConnection();
 823        this.transport = connection;
 824        if (connection == null) {
 825            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate");
 826            this.disconnectSocks5Connections();
 827            if (isInitiator()) {
 828                this.sendFallbackToIbb();
 829            }
 830        } else {
 831            //TODO at this point we can already close other connections to free some resources
 832            final JingleCandidate candidate = connection.getCandidate();
 833            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString());
 834            this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
 835            if (connection.needsActivation()) {
 836                if (connection.getCandidate().isOurs()) {
 837                    final String sid;
 838                    if (description.getVersion() == FileTransferDescription.Version.FT_3) {
 839                        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
 840                        sid = id.sessionId;
 841                    } else {
 842                        sid = getTransportId();
 843                    }
 844                    Log.d(Config.LOGTAG, "candidate "
 845                            + connection.getCandidate().getCid()
 846                            + " was our proxy. going to activate");
 847                    IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
 848                    activation.setTo(connection.getCandidate().getJid());
 849                    activation.query("http://jabber.org/protocol/bytestreams")
 850                            .setAttribute("sid", sid);
 851                    activation.query().addChild("activate")
 852                            .setContent(this.id.with.toEscapedString());
 853                    xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> {
 854                        if (response.getType() != IqPacket.TYPE.RESULT) {
 855                            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString());
 856                            sendProxyError();
 857                            onProxyActivated.failed();
 858                        } else {
 859                            sendProxyActivated(connection.getCandidate().getCid());
 860                            onProxyActivated.success();
 861                        }
 862                    });
 863                } else {
 864                    Log.d(Config.LOGTAG,
 865                            "candidate "
 866                                    + connection.getCandidate().getCid()
 867                                    + " was a proxy. waiting for other party to activate");
 868                }
 869            } else {
 870                if (isInitiator()) {
 871                    Log.d(Config.LOGTAG, "we were initiating. sending file");
 872                    connection.send(file, onFileTransmissionStatusChanged);
 873                } else {
 874                    Log.d(Config.LOGTAG, "we were responding. receiving file");
 875                    connection.receive(file, onFileTransmissionStatusChanged);
 876                }
 877            }
 878        }
 879    }
 880
 881    private JingleSocks5Transport chooseConnection() {
 882        final List<JingleSocks5Transport> establishedConnections = FluentIterable.from(connections.entrySet())
 883                .transform(Entry::getValue)
 884                .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs())))
 885                .toSortedList((a, b) -> {
 886                    final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority());
 887                    if (compare == 0) {
 888                        if (isInitiator()) {
 889                            //pick the one we sent a candidate-used for (meaning not ours)
 890                            return a.getCandidate().isOurs() ? 1 : -1;
 891                        } else {
 892                            //pick the one they sent a candidate-used for (meaning ours)
 893                            return a.getCandidate().isOurs() ? -1 : 1;
 894                        }
 895                    }
 896                    return compare;
 897                });
 898        return Iterables.getFirst(establishedConnections, null);
 899    }
 900
 901    private void sendSuccess() {
 902        sendSessionTerminate(Reason.SUCCESS);
 903        this.disconnectSocks5Connections();
 904        this.mJingleStatus = JINGLE_STATUS_FINISHED;
 905        this.message.setStatus(Message.STATUS_RECEIVED);
 906        this.message.setTransferable(null);
 907        this.xmppConnectionService.updateMessage(message, false);
 908        this.jingleConnectionManager.finishConnection(this);
 909    }
 910
 911    private void sendFallbackToIbb() {
 912        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
 913        final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
 914        final Content content = new Content(this.contentCreator, this.contentName);
 915        content.setSenders(this.contentSenders);
 916        this.transportId = JingleConnectionManager.nextRandomId();
 917        content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
 918        packet.addJingleContent(content);
 919        this.sendJinglePacket(packet);
 920    }
 921
 922
 923    private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) {
 924        if (isInitiator()) {
 925            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)");
 926            respondToIqWithOutOfOrder(packet);
 927            return;
 928        }
 929        final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
 930        if (!validState) {
 931            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace");
 932            respondToIqWithOutOfOrder(packet);
 933            return;
 934        }
 935        this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one;
 936        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb");
 937        final int remoteBlockSize = transportInfo.getBlockSize();
 938        if (remoteBlockSize > 0) {
 939            this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
 940        } else {
 941            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
 942        }
 943        this.transportId = transportInfo.getTransportId(); //TODO: handle the case where this is null by the remote party
 944        this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
 945
 946        final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
 947
 948        final Content content = new Content(contentCreator, contentName);
 949        content.setSenders(this.contentSenders);
 950        content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
 951        answer.addJingleContent(content);
 952
 953        respondToIq(packet, true);
 954
 955        if (isInitiator()) {
 956            this.sendJinglePacket(answer, (account, response) -> {
 957                if (response.getType() == IqPacket.TYPE.RESULT) {
 958                    Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
 959                    transport.connect(onIbbTransportConnected);
 960                }
 961            });
 962        } else {
 963            this.transport.receive(file, onFileTransmissionStatusChanged);
 964            this.sendJinglePacket(answer);
 965        }
 966    }
 967
 968    private void receiveTransportAccept(JinglePacket packet) {
 969        if (responding()) {
 970            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)");
 971            respondToIqWithOutOfOrder(packet);
 972            return;
 973        }
 974        final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
 975        if (!validState) {
 976            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept");
 977            respondToIqWithOutOfOrder(packet);
 978            return;
 979        }
 980        this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
 981        final Content content = packet.getJingleContent();
 982        final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
 983        if (transportInfo instanceof IbbTransportInfo) {
 984            final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
 985            final int remoteBlockSize = ibbTransportInfo.getBlockSize();
 986            if (remoteBlockSize > 0) {
 987                this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
 988            }
 989            final String sid = ibbTransportInfo.getTransportId();
 990            this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
 991
 992            if (sid == null || !sid.equals(this.transportId)) {
 993                Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId));
 994            }
 995            respondToIq(packet, true);
 996            //might be receive instead if we are not initiating
 997            if (isInitiator()) {
 998                this.transport.connect(onIbbTransportConnected);
 999            }
1000        } else {
1001            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept");
1002            respondToIq(packet, false);
1003        }
1004    }
1005
1006    private void receiveSuccess() {
1007        if (isInitiator()) {
1008            this.mJingleStatus = JINGLE_STATUS_FINISHED;
1009            this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
1010            this.disconnectSocks5Connections();
1011            if (this.transport instanceof JingleInBandTransport) {
1012                this.transport.disconnect();
1013            }
1014            this.message.setTransferable(null);
1015            this.jingleConnectionManager.finishConnection(this);
1016        } else {
1017            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding");
1018        }
1019    }
1020
1021    @Override
1022    public void cancel() {
1023        this.cancelled = true;
1024        abort(Reason.CANCEL);
1025    }
1026
1027    private void abort(final Reason reason) {
1028        this.disconnectSocks5Connections();
1029        if (this.transport instanceof JingleInBandTransport) {
1030            this.transport.disconnect();
1031        }
1032        sendSessionTerminate(reason);
1033        this.jingleConnectionManager.finishConnection(this);
1034        if (responding()) {
1035            this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1036            if (this.file != null) {
1037                file.delete();
1038            }
1039            this.jingleConnectionManager.updateConversationUi(true);
1040        } else {
1041            this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
1042            this.message.setTransferable(null);
1043        }
1044    }
1045
1046    private void fail() {
1047        fail(null);
1048    }
1049
1050    private void fail(String errorMessage) {
1051        this.mJingleStatus = JINGLE_STATUS_FAILED;
1052        this.disconnectSocks5Connections();
1053        if (this.transport instanceof JingleInBandTransport) {
1054            this.transport.disconnect();
1055        }
1056        FileBackend.close(mFileInputStream);
1057        FileBackend.close(mFileOutputStream);
1058        if (this.message != null) {
1059            if (responding()) {
1060                this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
1061                if (this.file != null) {
1062                    file.delete();
1063                }
1064                this.jingleConnectionManager.updateConversationUi(true);
1065            } else {
1066                this.xmppConnectionService.markMessage(this.message,
1067                        Message.STATUS_SEND_FAILED,
1068                        cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
1069                this.message.setTransferable(null);
1070            }
1071        }
1072        this.jingleConnectionManager.finishConnection(this);
1073    }
1074
1075    private void sendSessionTerminate(Reason reason) {
1076        final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
1077        packet.setReason(reason, null);
1078        this.sendJinglePacket(packet);
1079    }
1080
1081    private void connectNextCandidate() {
1082        for (JingleCandidate candidate : this.candidates) {
1083            if ((!connections.containsKey(candidate.getCid()) && (!candidate
1084                    .isOurs()))) {
1085                this.connectWithCandidate(candidate);
1086                return;
1087            }
1088        }
1089        this.sendCandidateError();
1090    }
1091
1092    private void connectWithCandidate(final JingleCandidate candidate) {
1093        final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
1094                this, candidate);
1095        connections.put(candidate.getCid(), socksConnection);
1096        socksConnection.connect(new OnTransportConnected() {
1097
1098            @Override
1099            public void failed() {
1100                Log.d(Config.LOGTAG,
1101                        "connection failed with " + candidate.getHost() + ":"
1102                                + candidate.getPort());
1103                connectNextCandidate();
1104            }
1105
1106            @Override
1107            public void established() {
1108                Log.d(Config.LOGTAG,
1109                        "established connection with " + candidate.getHost()
1110                                + ":" + candidate.getPort());
1111                sendCandidateUsed(candidate.getCid());
1112            }
1113        });
1114    }
1115
1116    private void disconnectSocks5Connections() {
1117        Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
1118                .entrySet().iterator();
1119        while (it.hasNext()) {
1120            Entry<String, JingleSocks5Transport> pairs = it.next();
1121            pairs.getValue().disconnect();
1122            it.remove();
1123        }
1124    }
1125
1126    private void sendProxyActivated(String cid) {
1127        final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1128        final Content content = new Content(this.contentCreator, this.contentName);
1129        content.setSenders(this.contentSenders);
1130        content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
1131        packet.addJingleContent(content);
1132        this.sendJinglePacket(packet);
1133    }
1134
1135    private void sendProxyError() {
1136        final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1137        final Content content = new Content(this.contentCreator, this.contentName);
1138        content.setSenders(this.contentSenders);
1139        content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
1140        packet.addJingleContent(content);
1141        this.sendJinglePacket(packet);
1142    }
1143
1144    private void sendCandidateUsed(final String cid) {
1145        JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1146        final Content content = new Content(this.contentCreator, this.contentName);
1147        content.setSenders(this.contentSenders);
1148        content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
1149        packet.addJingleContent(content);
1150        this.sentCandidate = true;
1151        if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
1152            connect();
1153        }
1154        this.sendJinglePacket(packet);
1155    }
1156
1157    private void sendCandidateError() {
1158        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
1159        JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
1160        Content content = new Content(this.contentCreator, this.contentName);
1161        content.setSenders(this.contentSenders);
1162        content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
1163        packet.addJingleContent(content);
1164        this.sentCandidate = true;
1165        this.sendJinglePacket(packet);
1166        if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
1167            connect();
1168        }
1169    }
1170
1171    private int getJingleStatus() {
1172        return this.mJingleStatus;
1173    }
1174
1175    private boolean equalCandidateExists(JingleCandidate candidate) {
1176        for (JingleCandidate c : this.candidates) {
1177            if (c.equalValues(candidate)) {
1178                return true;
1179            }
1180        }
1181        return false;
1182    }
1183
1184    private void mergeCandidate(JingleCandidate candidate) {
1185        for (JingleCandidate c : this.candidates) {
1186            if (c.equals(candidate)) {
1187                return;
1188            }
1189        }
1190        this.candidates.add(candidate);
1191    }
1192
1193    private void mergeCandidates(List<JingleCandidate> candidates) {
1194        Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
1195        for (JingleCandidate c : candidates) {
1196            mergeCandidate(c);
1197        }
1198    }
1199
1200    private JingleCandidate getCandidate(String cid) {
1201        for (JingleCandidate c : this.candidates) {
1202            if (c.getCid().equals(cid)) {
1203                return c;
1204            }
1205        }
1206        return null;
1207    }
1208
1209    void updateProgress(int i) {
1210        this.mProgress = i;
1211        jingleConnectionManager.updateConversationUi(false);
1212    }
1213
1214    String getTransportId() {
1215        return this.transportId;
1216    }
1217
1218    FileTransferDescription.Version getFtVersion() {
1219        return this.description.getVersion();
1220    }
1221
1222    public JingleTransport getTransport() {
1223        return this.transport;
1224    }
1225
1226    public boolean start() {
1227        if (id.account.getStatus() == Account.State.ONLINE) {
1228            if (mJingleStatus == JINGLE_STATUS_INITIATED) {
1229                new Thread(this::sendAccept).start();
1230            }
1231            return true;
1232        } else {
1233            return false;
1234        }
1235    }
1236
1237    @Override
1238    public int getStatus() {
1239        return this.mStatus;
1240    }
1241
1242    @Override
1243    public Long getFileSize() {
1244        if (this.file != null) {
1245            return this.file.getExpectedSize();
1246        } else {
1247            return null;
1248        }
1249    }
1250
1251    @Override
1252    public int getProgress() {
1253        return this.mProgress;
1254    }
1255
1256    AbstractConnectionManager getConnectionManager() {
1257        return this.jingleConnectionManager;
1258    }
1259
1260    interface OnProxyActivated {
1261        void success();
1262
1263        void failed();
1264    }
1265}