XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
   4
   5import android.content.Context;
   6import android.graphics.Bitmap;
   7import android.graphics.BitmapFactory;
   8import android.os.Build;
   9import android.os.SystemClock;
  10import android.security.KeyChain;
  11import android.util.Base64;
  12import android.util.Log;
  13import android.util.Pair;
  14import android.util.SparseArray;
  15import androidx.annotation.NonNull;
  16import androidx.annotation.Nullable;
  17import com.google.common.base.MoreObjects;
  18import com.google.common.base.Optional;
  19import com.google.common.base.Preconditions;
  20import com.google.common.base.Strings;
  21import com.google.common.base.Throwables;
  22import com.google.common.collect.ClassToInstanceMap;
  23import com.google.common.collect.ImmutableList;
  24import com.google.common.collect.Iterables;
  25import com.google.common.primitives.Ints;
  26import com.google.common.util.concurrent.FutureCallback;
  27import com.google.common.util.concurrent.Futures;
  28import com.google.common.util.concurrent.ListenableFuture;
  29import com.google.common.util.concurrent.MoreExecutors;
  30import com.google.common.util.concurrent.SettableFuture;
  31import de.gultsch.common.Patterns;
  32import eu.siacs.conversations.AppSettings;
  33import eu.siacs.conversations.BuildConfig;
  34import eu.siacs.conversations.Config;
  35import eu.siacs.conversations.R;
  36import eu.siacs.conversations.crypto.XmppDomainVerifier;
  37import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  38import eu.siacs.conversations.crypto.sasl.ChannelBinding;
  39import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
  40import eu.siacs.conversations.crypto.sasl.DowngradeProtection;
  41import eu.siacs.conversations.crypto.sasl.HashedToken;
  42import eu.siacs.conversations.crypto.sasl.SaslMechanism;
  43import eu.siacs.conversations.crypto.sasl.ScramMechanism;
  44import eu.siacs.conversations.entities.Account;
  45import eu.siacs.conversations.entities.Message;
  46import eu.siacs.conversations.generator.IqGenerator;
  47import eu.siacs.conversations.http.HttpConnectionManager;
  48import eu.siacs.conversations.parser.IqParser;
  49import eu.siacs.conversations.parser.MessageParser;
  50import eu.siacs.conversations.parser.PresenceParser;
  51import eu.siacs.conversations.persistance.DatabaseBackend;
  52import eu.siacs.conversations.persistance.FileBackend;
  53import eu.siacs.conversations.services.MemorizingTrustManager;
  54import eu.siacs.conversations.services.MessageArchiveService;
  55import eu.siacs.conversations.services.NotificationService;
  56import eu.siacs.conversations.services.XmppConnectionService;
  57import eu.siacs.conversations.ui.util.PendingItem;
  58import eu.siacs.conversations.utils.AccountUtils;
  59import eu.siacs.conversations.utils.CryptoHelper;
  60import eu.siacs.conversations.utils.PhoneHelper;
  61import eu.siacs.conversations.utils.Resolver;
  62import eu.siacs.conversations.utils.SSLSockets;
  63import eu.siacs.conversations.utils.SocksSocketFactory;
  64import eu.siacs.conversations.utils.XmlHelper;
  65import eu.siacs.conversations.xml.Element;
  66import eu.siacs.conversations.xml.LocalizedContent;
  67import eu.siacs.conversations.xml.Namespace;
  68import eu.siacs.conversations.xml.Tag;
  69import eu.siacs.conversations.xml.TagWriter;
  70import eu.siacs.conversations.xml.XmlReader;
  71import eu.siacs.conversations.xmpp.bind.Bind2;
  72import eu.siacs.conversations.xmpp.forms.Data;
  73import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  74import eu.siacs.conversations.xmpp.manager.AbstractManager;
  75import eu.siacs.conversations.xmpp.manager.DiscoManager;
  76import im.conversations.android.xmpp.Entity;
  77import im.conversations.android.xmpp.model.AuthenticationFailure;
  78import im.conversations.android.xmpp.model.AuthenticationRequest;
  79import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
  80import im.conversations.android.xmpp.model.Extension;
  81import im.conversations.android.xmpp.model.StreamElement;
  82import im.conversations.android.xmpp.model.bind2.Bind;
  83import im.conversations.android.xmpp.model.bind2.Bound;
  84import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
  85import im.conversations.android.xmpp.model.csi.Active;
  86import im.conversations.android.xmpp.model.csi.Inactive;
  87import im.conversations.android.xmpp.model.disco.info.InfoQuery;
  88import im.conversations.android.xmpp.model.error.Condition;
  89import im.conversations.android.xmpp.model.fast.Fast;
  90import im.conversations.android.xmpp.model.fast.RequestToken;
  91import im.conversations.android.xmpp.model.jingle.Jingle;
  92import im.conversations.android.xmpp.model.sasl.Auth;
  93import im.conversations.android.xmpp.model.sasl.Failure;
  94import im.conversations.android.xmpp.model.sasl.Mechanisms;
  95import im.conversations.android.xmpp.model.sasl.Response;
  96import im.conversations.android.xmpp.model.sasl.SaslError;
  97import im.conversations.android.xmpp.model.sasl.Success;
  98import im.conversations.android.xmpp.model.sasl2.Authenticate;
  99import im.conversations.android.xmpp.model.sasl2.Authentication;
 100import im.conversations.android.xmpp.model.sasl2.UserAgent;
 101import im.conversations.android.xmpp.model.sm.Ack;
 102import im.conversations.android.xmpp.model.sm.Enable;
 103import im.conversations.android.xmpp.model.sm.Enabled;
 104import im.conversations.android.xmpp.model.sm.Failed;
 105import im.conversations.android.xmpp.model.sm.Request;
 106import im.conversations.android.xmpp.model.sm.Resume;
 107import im.conversations.android.xmpp.model.sm.Resumed;
 108import im.conversations.android.xmpp.model.sm.StreamManagement;
 109import im.conversations.android.xmpp.model.stanza.Iq;
 110import im.conversations.android.xmpp.model.stanza.Presence;
 111import im.conversations.android.xmpp.model.stanza.Stanza;
 112import im.conversations.android.xmpp.model.streams.StreamError;
 113import im.conversations.android.xmpp.model.tls.Proceed;
 114import im.conversations.android.xmpp.model.tls.StartTls;
 115import im.conversations.android.xmpp.processor.BindProcessor;
 116import java.io.ByteArrayInputStream;
 117import java.io.IOException;
 118import java.io.InputStream;
 119import java.net.ConnectException;
 120import java.net.IDN;
 121import java.net.InetAddress;
 122import java.net.InetSocketAddress;
 123import java.net.Socket;
 124import java.net.UnknownHostException;
 125import java.security.KeyManagementException;
 126import java.security.NoSuchAlgorithmException;
 127import java.security.Principal;
 128import java.security.PrivateKey;
 129import java.security.cert.X509Certificate;
 130import java.util.ArrayList;
 131import java.util.Arrays;
 132import java.util.Collection;
 133import java.util.Collections;
 134import java.util.HashMap;
 135import java.util.HashSet;
 136import java.util.Hashtable;
 137import java.util.Iterator;
 138import java.util.List;
 139import java.util.Map;
 140import java.util.Map.Entry;
 141import java.util.Set;
 142import java.util.concurrent.CountDownLatch;
 143import java.util.concurrent.TimeUnit;
 144import java.util.concurrent.TimeoutException;
 145import java.util.concurrent.atomic.AtomicBoolean;
 146import java.util.concurrent.atomic.AtomicInteger;
 147import java.util.function.Consumer;
 148import java.util.regex.Matcher;
 149import javax.net.ssl.KeyManager;
 150import javax.net.ssl.SSLContext;
 151import javax.net.ssl.SSLPeerUnverifiedException;
 152import javax.net.ssl.SSLSocket;
 153import javax.net.ssl.SSLSocketFactory;
 154import javax.net.ssl.X509KeyManager;
 155import javax.net.ssl.X509TrustManager;
 156import okhttp3.HttpUrl;
 157import org.xmlpull.v1.XmlPullParserException;
 158
 159public class XmppConnection implements Runnable {
 160
 161    protected final Account account;
 162    private final Features features = new Features(this);
 163    private final HashMap<String, Jid> commands = new HashMap<>();
 164    private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
 165    private final Hashtable<String, Pair<Iq, Consumer<Iq>>> packetCallbacks = new Hashtable<>();
 166    private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
 167            new HashSet<>();
 168    private final AppSettings appSettings;
 169    private final XmppConnectionService mXmppConnectionService;
 170    private Socket socket;
 171    private XmlReader tagReader;
 172    private TagWriter tagWriter = new TagWriter();
 173    private boolean shouldAuthenticate = true;
 174    private boolean inSmacksSession = false;
 175    private boolean quickStartInProgress = false;
 176    private boolean isBound = false;
 177    private boolean offlineMessagesRetrieved = false;
 178    private im.conversations.android.xmpp.model.streams.Features streamFeatures;
 179    private im.conversations.android.xmpp.model.streams.Features boundStreamFeatures;
 180    private StreamId streamId = null;
 181    private int stanzasReceived = 0;
 182    private int stanzasSent = 0;
 183    private int stanzasSentBeforeAuthentication;
 184    private long lastPacketReceived = 0;
 185    private long lastPingSent = 0;
 186    private long lastConnectionStarted = 0;
 187    private long lastSessionStarted = 0;
 188    private long lastDiscoStarted = 0;
 189    private boolean isMamPreferenceAlways = false;
 190    private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
 191    private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
 192    private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
 193    private boolean mInteractive = false;
 194    private int attempt = 0;
 195    private OnJinglePacketReceived jingleListener = null;
 196
 197    private final Consumer<Presence> presenceListener;
 198    private final Consumer<Iq> unregisteredIqListener;
 199    private final Consumer<im.conversations.android.xmpp.model.stanza.Message> messageListener;
 200    private OnStatusChanged statusListener = null;
 201    private final Runnable bindListener;
 202    private OnMessageAcknowledged acknowledgedListener = null;
 203    private final PendingItem<String> pendingResumeId = new PendingItem<>();
 204    private LoginInfo loginInfo;
 205    private HashedToken.Mechanism hashTokenRequest;
 206    private HttpUrl redirectionUrl = null;
 207    private String verifiedHostname = null;
 208    private Resolver.Result currentResolverResult;
 209    private Resolver.Result seeOtherHostResolverResult;
 210    private volatile Thread mThread;
 211    private CountDownLatch mStreamCountDownLatch;
 212    private final ClassToInstanceMap<AbstractManager> managers;
 213
 214    public XmppConnection(final Account account, final XmppConnectionService service) {
 215        this.account = account;
 216        this.mXmppConnectionService = service;
 217        this.appSettings = mXmppConnectionService.getAppSettings();
 218        this.presenceListener = new PresenceParser(service, this);
 219        this.unregisteredIqListener = new IqParser(service, this);
 220        this.messageListener = new MessageParser(service, this);
 221        this.bindListener = new BindProcessor(service, this);
 222        this.managers = Managers.get(service.getApplicationContext(), this);
 223    }
 224
 225    private static void fixResource(final Context context, final Account account) {
 226        String resource = account.getResource();
 227        int fixedPartLength =
 228                context.getString(R.string.app_name).length() + 1; // include the trailing dot
 229        int randomPartLength = 4; // 3 bytes
 230        if (resource != null && resource.length() > fixedPartLength + randomPartLength) {
 231            if (validBase64(
 232                    resource.substring(fixedPartLength, fixedPartLength + randomPartLength))) {
 233                account.setResource(resource.substring(0, fixedPartLength + randomPartLength));
 234            }
 235        }
 236    }
 237
 238    private static boolean validBase64(final String input) {
 239        try {
 240            return Base64.decode(input, Base64.URL_SAFE).length == 3;
 241        } catch (final Throwable throwable) {
 242            return false;
 243        }
 244    }
 245
 246    private void changeStatus(final Account.State nextStatus) {
 247        synchronized (this) {
 248            if (Thread.currentThread().isInterrupted()) {
 249                Log.d(
 250                        Config.LOGTAG,
 251                        account.getJid().asBareJid()
 252                                + ": not changing status to "
 253                                + nextStatus
 254                                + " because thread was interrupted");
 255                return;
 256            }
 257            if (account.getStatus() != nextStatus) {
 258                if (nextStatus == Account.State.OFFLINE
 259                        && account.getStatus() != Account.State.CONNECTING
 260                        && account.getStatus() != Account.State.ONLINE
 261                        && account.getStatus() != Account.State.DISABLED
 262                        && account.getStatus() != Account.State.LOGGED_OUT) {
 263                    return;
 264                }
 265                if (nextStatus == Account.State.ONLINE) {
 266                    this.attempt = 0;
 267                }
 268                account.setStatus(nextStatus);
 269            } else {
 270                return;
 271            }
 272        }
 273        if (statusListener != null) {
 274            statusListener.onStatusChanged(account);
 275        }
 276    }
 277
 278    public Jid getJidForCommand(final String node) {
 279        synchronized (this.commands) {
 280            return this.commands.get(node);
 281        }
 282    }
 283
 284    public void prepareNewConnection() {
 285        this.lastConnectionStarted = SystemClock.elapsedRealtime();
 286        this.lastPingSent = SystemClock.elapsedRealtime();
 287        this.lastDiscoStarted = Long.MAX_VALUE;
 288        this.mWaitingForSmCatchup.set(false);
 289        this.changeStatus(Account.State.CONNECTING);
 290    }
 291
 292    public boolean isWaitingForSmCatchup() {
 293        return mWaitingForSmCatchup.get();
 294    }
 295
 296    public void incrementSmCatchupMessageCounter() {
 297        this.mSmCatchupMessageCounter.incrementAndGet();
 298    }
 299
 300    protected void connect() {
 301        if (mXmppConnectionService.areMessagesInitialized()) {
 302            mXmppConnectionService.resetSendingToWaiting(account);
 303        }
 304        Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
 305        this.streamFeatures = null;
 306        this.pendingResumeId.clear();
 307        this.loginInfo = null;
 308        this.features.encryptionEnabled = false;
 309        this.inSmacksSession = false;
 310        this.quickStartInProgress = false;
 311        this.isBound = false;
 312        this.attempt++;
 313        this.currentResolverResult = null;
 314        // will be set if user entered hostname is being used or hostname was verified with dnssec
 315        this.verifiedHostname = null;
 316        try {
 317            Socket localSocket;
 318            shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
 319            this.changeStatus(Account.State.CONNECTING);
 320            final boolean useTorSetting = appSettings.isUseTor();
 321            final boolean extended = appSettings.isExtendedConnectionOptions();
 322            final boolean useTor = useTorSetting || account.isOnion();
 323            // TODO collapse Tor usage into normal connection code path
 324            if (useTor) {
 325                final var seeOtherHost = this.seeOtherHostResolverResult;
 326                final var hostname = account.getHostname().trim();
 327                final var port = account.getPort();
 328                final Resolver.Result resume = streamId == null ? null : streamId.location;
 329                final Resolver.Result viaTor;
 330                if (resume != null) {
 331                    viaTor = resume;
 332                } else if (seeOtherHost != null) {
 333                    viaTor = seeOtherHost;
 334                } else if (hostname.isEmpty() || port < 0) {
 335                    viaTor =
 336                            Iterables.getOnlyElement(
 337                                    Resolver.fromHardCoded(
 338                                            account.getServer(), Resolver.XMPP_PORT_STARTTLS));
 339                } else {
 340                    if (useTorSetting || extended) {
 341                        // if the hostname configuration is showing we can take it
 342                        viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port));
 343                    } else {
 344                        viaTor =
 345                                Iterables.getOnlyElement(
 346                                        Resolver.fromHardCoded(
 347                                                account.getServer(), Resolver.XMPP_PORT_STARTTLS));
 348                    }
 349                    this.verifiedHostname = hostname;
 350                }
 351
 352                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " via Tor: " + viaTor);
 353
 354                localSocket =
 355                        SocksSocketFactory.createSocketOverTor(
 356                                viaTor.asDestination(), viaTor.getPort());
 357
 358                if (viaTor.isDirectTls()) {
 359                    localSocket = upgradeSocketToTls(localSocket);
 360                    features.encryptionEnabled = true;
 361                }
 362
 363                try {
 364                    if (startXmpp(localSocket)) {
 365                        this.currentResolverResult = viaTor;
 366                        this.seeOtherHostResolverResult = null;
 367                    }
 368                } catch (final InterruptedException e) {
 369                    Log.d(
 370                            Config.LOGTAG,
 371                            account.getJid().asBareJid()
 372                                    + ": thread was interrupted before beginning stream");
 373                    return;
 374                } catch (final Exception e) {
 375                    throw new IOException("Could not start stream", e);
 376                }
 377            } else {
 378                final var hostname = account.getHostname().trim();
 379                final String domain = account.getServer();
 380                final List<Resolver.Result> results = new ArrayList<>();
 381                final boolean hardcoded = extended && !hostname.isEmpty();
 382                if (hardcoded) {
 383                    results.addAll(Resolver.fromHardCoded(hostname, account.getPort()));
 384                } else {
 385                    results.addAll(Resolver.resolve(domain));
 386                }
 387                if (Thread.currentThread().isInterrupted()) {
 388                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
 389                    return;
 390                }
 391                if (results.isEmpty()) {
 392                    Log.e(
 393                            Config.LOGTAG,
 394                            account.getJid().asBareJid() + ": Resolver results were empty");
 395                    return;
 396                }
 397                final Resolver.Result storedBackupResult;
 398                if (hardcoded) {
 399                    storedBackupResult = null;
 400                } else {
 401                    storedBackupResult =
 402                            mXmppConnectionService.databaseBackend.findResolverResult(domain);
 403                    if (storedBackupResult != null && !results.contains(storedBackupResult)) {
 404                        results.add(storedBackupResult);
 405                        Log.d(
 406                                Config.LOGTAG,
 407                                account.getJid().asBareJid()
 408                                        + ": loaded backup resolver result from db: "
 409                                        + storedBackupResult);
 410                    }
 411                }
 412                final StreamId streamId = this.streamId;
 413                final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
 414                if (resumeLocation != null) {
 415                    Log.d(
 416                            Config.LOGTAG,
 417                            account.getJid().asBareJid()
 418                                    + ": injected resume location on position 0");
 419                    results.add(0, resumeLocation);
 420                }
 421                final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
 422                if (seeOtherHost != null) {
 423                    Log.d(
 424                            Config.LOGTAG,
 425                            account.getJid().asBareJid()
 426                                    + ": injected see-other-host on position 0");
 427                    results.add(0, seeOtherHost);
 428                }
 429                for (final Iterator<Resolver.Result> iterator = results.iterator();
 430                        iterator.hasNext(); ) {
 431                    final Resolver.Result result = iterator.next();
 432                    if (Thread.currentThread().isInterrupted()) {
 433                        Log.d(
 434                                Config.LOGTAG,
 435                                account.getJid().asBareJid() + ": Thread was interrupted");
 436                        return;
 437                    }
 438                    try {
 439                        // if tls is true, encryption is implied and must not be started
 440                        features.encryptionEnabled = result.isDirectTls();
 441                        verifiedHostname =
 442                                result.isAuthenticated() ? result.getHostname().toString() : null;
 443                        final InetSocketAddress addr;
 444                        if (result.getIp() != null) {
 445                            addr = new InetSocketAddress(result.getIp(), result.getPort());
 446                            Log.d(
 447                                    Config.LOGTAG,
 448                                    account.getJid().asBareJid().toString()
 449                                            + ": using values from resolver "
 450                                            + (result.getHostname() == null
 451                                                    ? ""
 452                                                    : result.getHostname().toString() + "/")
 453                                            + result.getIp().getHostAddress()
 454                                            + ":"
 455                                            + result.getPort()
 456                                            + " tls: "
 457                                            + features.encryptionEnabled);
 458                        } else {
 459                            addr =
 460                                    new InetSocketAddress(
 461                                            IDN.toASCII(result.getHostname().toString()),
 462                                            result.getPort());
 463                            Log.d(
 464                                    Config.LOGTAG,
 465                                    account.getJid().asBareJid().toString()
 466                                            + ": using values from resolver "
 467                                            + result.getHostname().toString()
 468                                            + ":"
 469                                            + result.getPort()
 470                                            + " tls: "
 471                                            + features.encryptionEnabled);
 472                        }
 473
 474                        localSocket = new Socket();
 475                        localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
 476                        localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
 477                        if (features.encryptionEnabled) {
 478                            localSocket = upgradeSocketToTls(localSocket);
 479                        }
 480                        if (startXmpp(localSocket)) {
 481                            // reset to 0; once the connection is established we don't want this
 482                            localSocket.setSoTimeout(0);
 483                            if (!hardcoded && !result.equals(storedBackupResult)) {
 484                                mXmppConnectionService.databaseBackend.saveResolverResult(
 485                                        domain, result);
 486                            }
 487                            this.currentResolverResult = result;
 488                            this.seeOtherHostResolverResult = null;
 489                            break; // successfully connected to server that speaks xmpp
 490                        } else {
 491                            FileBackend.close(localSocket);
 492                            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
 493                        }
 494                    } catch (final StateChangingException e) {
 495                        if (!iterator.hasNext()) {
 496                            throw e;
 497                        }
 498                    } catch (InterruptedException e) {
 499                        Log.d(
 500                                Config.LOGTAG,
 501                                account.getJid().asBareJid()
 502                                        + ": thread was interrupted before beginning stream");
 503                        return;
 504                    } catch (final Throwable e) {
 505                        Log.d(
 506                                Config.LOGTAG,
 507                                account.getJid().asBareJid().toString()
 508                                        + ": "
 509                                        + e.getMessage()
 510                                        + "("
 511                                        + e.getClass().getName()
 512                                        + ")");
 513                        if (!iterator.hasNext()) {
 514                            throw new UnknownHostException();
 515                        }
 516                    }
 517                }
 518            }
 519            processStream();
 520        } catch (final SecurityException e) {
 521            this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
 522        } catch (final StateChangingException e) {
 523            this.changeStatus(e.state);
 524        } catch (final UnknownHostException
 525                | ConnectException
 526                | SocksSocketFactory.HostNotFoundException e) {
 527            this.changeStatus(Account.State.SERVER_NOT_FOUND);
 528        } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
 529            this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
 530        } catch (final IOException | XmlPullParserException e) {
 531            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
 532            this.changeStatus(Account.State.OFFLINE);
 533            this.attempt = Math.max(0, this.attempt - 1);
 534        } finally {
 535            if (!Thread.currentThread().isInterrupted()) {
 536                forceCloseSocket();
 537            } else {
 538                Log.d(
 539                        Config.LOGTAG,
 540                        account.getJid().asBareJid()
 541                                + ": not force closing socket because thread was interrupted");
 542            }
 543        }
 544    }
 545
 546    /**
 547     * Starts xmpp protocol, call after connecting to socket
 548     *
 549     * @return true if server returns with valid xmpp, false otherwise
 550     */
 551    private boolean startXmpp(final Socket socket) throws Exception {
 552        if (Thread.currentThread().isInterrupted()) {
 553            throw new InterruptedException();
 554        }
 555        // this means we have at least found a socket to connect to. give the connection another 90s
 556        this.lastConnectionStarted = SystemClock.elapsedRealtime();
 557        this.socket = socket;
 558        this.tagReader = new XmlReader();
 559        if (tagWriter != null) {
 560            tagWriter.forceClose();
 561        }
 562        this.tagWriter = new TagWriter();
 563        this.tagWriter.setOutputStream(socket.getOutputStream());
 564        this.tagReader.setInputStream(socket.getInputStream());
 565        this.tagWriter.beginDocument();
 566        final boolean quickStart;
 567        if (socket instanceof SSLSocket sslSocket) {
 568            SSLSockets.log(account, sslSocket);
 569            quickStart = establishStream(SSLSockets.version(sslSocket));
 570        } else {
 571            quickStart = establishStream(SSLSockets.Version.NONE);
 572        }
 573        final Tag tag = tagReader.readTag();
 574        if (Thread.currentThread().isInterrupted()) {
 575            throw new InterruptedException();
 576        }
 577        if (tag == null) {
 578            return false;
 579        }
 580        final boolean success = tag.isStart("stream", Namespace.STREAMS);
 581        if (success) {
 582            final var from = tag.getAttribute("from");
 583            if (from == null || !from.equals(account.getServer())) {
 584                throw new StateChangingException(Account.State.HOST_UNKNOWN);
 585            }
 586        }
 587        if (success && quickStart) {
 588            this.quickStartInProgress = true;
 589        }
 590        return success;
 591    }
 592
 593    private SSLSocketFactory getSSLSocketFactory()
 594            throws NoSuchAlgorithmException, KeyManagementException {
 595        final SSLContext sc = SSLSockets.getSSLContext();
 596        final MemorizingTrustManager trustManager =
 597                this.mXmppConnectionService.getMemorizingTrustManager();
 598        final KeyManager[] keyManager;
 599        if (account.getPrivateKeyAlias() != null) {
 600            keyManager = new KeyManager[] {new MyKeyManager()};
 601        } else {
 602            keyManager = null;
 603        }
 604        final String domain = account.getServer();
 605        sc.init(
 606                keyManager,
 607                new X509TrustManager[] {
 608                    mInteractive
 609                            ? trustManager.getInteractive(domain)
 610                            : trustManager.getNonInteractive(domain)
 611                },
 612                SECURE_RANDOM);
 613        return sc.getSocketFactory();
 614    }
 615
 616    @Override
 617    public void run() {
 618        synchronized (this) {
 619            this.mThread = Thread.currentThread();
 620            if (this.mThread.isInterrupted()) {
 621                Log.d(
 622                        Config.LOGTAG,
 623                        account.getJid().asBareJid()
 624                                + ": aborting connect because thread was interrupted");
 625                return;
 626            }
 627            forceCloseSocket();
 628        }
 629        connect();
 630    }
 631
 632    private void processStream() throws XmlPullParserException, IOException {
 633        final CountDownLatch streamCountDownLatch = new CountDownLatch(1);
 634        this.mStreamCountDownLatch = streamCountDownLatch;
 635        Tag nextTag = tagReader.readTag();
 636        while (nextTag != null && !nextTag.isEnd("stream")) {
 637            if (nextTag.isStart("error", Namespace.STREAMS)) {
 638                processStreamError(tagReader.readElement(nextTag, StreamError.class));
 639            } else if (nextTag.isStart("features", Namespace.STREAMS)) {
 640                processStreamFeatures(nextTag);
 641            } else if (nextTag.isStart("proceed", Namespace.TLS)) {
 642                if (this.socket instanceof SSLSocket) {
 643                    throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 644                }
 645                switchOverToTls(nextTag);
 646            } else if (nextTag.isStart("failure", Namespace.TLS)) {
 647                throw new StateChangingException(Account.State.TLS_ERROR);
 648            } else if (!isSecure()) {
 649                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 650            } else if (account.isOptionSet(Account.OPTION_REGISTER)
 651                    && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 652                processIq(nextTag);
 653            } else if (this.loginInfo == null) {
 654                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 655            } else if (nextTag.isStart("success", Namespace.SASL)) {
 656                processSuccess(tagReader.readElement(nextTag, Success.class));
 657                break;
 658            } else if (nextTag.isStart("success", Namespace.SASL_2)) {
 659                processSuccess(
 660                        tagReader.readElement(
 661                                nextTag, im.conversations.android.xmpp.model.sasl2.Success.class));
 662            } else if (nextTag.isStart("failure", Namespace.SASL)) {
 663                final var failure = tagReader.readElement(nextTag, Failure.class);
 664                processFailure(failure);
 665            } else if (nextTag.isStart("failure", Namespace.SASL_2)) {
 666                final var failure =
 667                        tagReader.readElement(
 668                                nextTag, im.conversations.android.xmpp.model.sasl2.Failure.class);
 669                processFailure(failure);
 670            } else if (nextTag.isStart("continue", Namespace.SASL_2)) {
 671                // two step sasl2 - we don’t support this yet
 672                throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
 673            } else if (nextTag.isStart("challenge")) {
 674                final Element challenge = tagReader.readElement(nextTag);
 675                processChallenge(challenge);
 676            } else if (!LoginInfo.isSuccess(this.loginInfo)) {
 677                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 678            } else if (this.streamId != null
 679                    && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
 680                final Resumed resumed = tagReader.readElement(nextTag, Resumed.class);
 681                processResumed(resumed);
 682            } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
 683                final Failed failed = tagReader.readElement(nextTag, Failed.class);
 684                processFailed(failed, true);
 685            } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
 686                processIq(nextTag);
 687            } else if (!isBound) {
 688                Log.d(
 689                        Config.LOGTAG,
 690                        account.getJid().asBareJid()
 691                                + ": server sent unexpected"
 692                                + nextTag.identifier());
 693                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 694            } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
 695                processMessage(nextTag);
 696            } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
 697                processPresence(nextTag);
 698            } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
 699                final var enabled = tagReader.readElement(nextTag, Enabled.class);
 700                processEnabled(enabled);
 701            } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
 702                tagReader.readElement(nextTag);
 703                if (Config.EXTENDED_SM_LOGGING) {
 704                    Log.d(
 705                            Config.LOGTAG,
 706                            account.getJid().asBareJid()
 707                                    + ": acknowledging stanza #"
 708                                    + this.stanzasReceived);
 709                }
 710                final Ack ack = new Ack(this.stanzasReceived);
 711                tagWriter.writeStanzaAsync(ack);
 712            } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
 713                boolean accountUiNeedsRefresh = false;
 714                synchronized (NotificationService.CATCHUP_LOCK) {
 715                    if (mWaitingForSmCatchup.compareAndSet(true, false)) {
 716                        final int messageCount = mSmCatchupMessageCounter.get();
 717                        final int pendingIQs = packetCallbacks.size();
 718                        Log.d(
 719                                Config.LOGTAG,
 720                                account.getJid().asBareJid()
 721                                        + ": SM catchup complete (messages="
 722                                        + messageCount
 723                                        + ", pending IQs="
 724                                        + pendingIQs
 725                                        + ")");
 726                        accountUiNeedsRefresh = true;
 727                        if (messageCount > 0) {
 728                            mXmppConnectionService
 729                                    .getNotificationService()
 730                                    .finishBacklog(true, account);
 731                        }
 732                    }
 733                }
 734                if (accountUiNeedsRefresh) {
 735                    mXmppConnectionService.updateAccountUi();
 736                }
 737                final var ack = tagReader.readElement(nextTag, Ack.class);
 738                lastPacketReceived = SystemClock.elapsedRealtime();
 739                final boolean acknowledgedMessages;
 740                synchronized (this.mStanzaQueue) {
 741                    final Optional<Integer> serverSequence = ack.getHandled();
 742                    if (serverSequence.isPresent()) {
 743                        acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get());
 744                    } else {
 745                        acknowledgedMessages = false;
 746                        Log.d(
 747                                Config.LOGTAG,
 748                                account.getJid().asBareJid()
 749                                        + ": server send ack without sequence number");
 750                    }
 751                }
 752                if (acknowledgedMessages) {
 753                    mXmppConnectionService.updateConversationUi();
 754                }
 755            } else {
 756                Log.e(
 757                        Config.LOGTAG,
 758                        account.getJid().asBareJid()
 759                                + ": Encountered unknown stream element"
 760                                + nextTag.identifier());
 761                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 762            }
 763            nextTag = tagReader.readTag();
 764        }
 765        if (nextTag != null && nextTag.isEnd("stream")) {
 766            streamCountDownLatch.countDown();
 767        }
 768    }
 769
 770    private void processChallenge(final Element challenge) throws IOException {
 771        final SaslMechanism.Version version;
 772        try {
 773            version = SaslMechanism.Version.of(challenge);
 774        } catch (final IllegalArgumentException e) {
 775            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 776        }
 777        final StreamElement response;
 778        if (version == SaslMechanism.Version.SASL) {
 779            response = new Response();
 780        } else if (version == SaslMechanism.Version.SASL_2) {
 781            response = new im.conversations.android.xmpp.model.sasl2.Response();
 782        } else {
 783            throw new AssertionError("Missing implementation for " + version);
 784        }
 785        final LoginInfo currentLoginInfo = this.loginInfo;
 786        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
 787            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 788        }
 789        try {
 790            response.setContent(
 791                    currentLoginInfo.saslMechanism.getResponse(
 792                            challenge.getContent(), sslSocketOrNull(socket)));
 793        } catch (final SaslMechanism.AuthenticationException e) {
 794            // TODO: Send auth abort tag.
 795            Log.e(Config.LOGTAG, e.toString());
 796            throw new StateChangingException(Account.State.UNAUTHORIZED);
 797        }
 798        tagWriter.writeElement(response);
 799    }
 800
 801    private void processSuccess(final StreamElement element)
 802            throws IOException, XmlPullParserException {
 803        final LoginInfo currentLoginInfo = this.loginInfo;
 804        final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
 805        if (currentLoginInfo == null
 806                || LoginInfo.isSuccess(currentLoginInfo)
 807                || currentSaslMechanism == null) {
 808            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 809        }
 810        final SaslMechanism.Version version;
 811        final String challenge;
 812        if (element instanceof Success success) {
 813            challenge = success.getContent();
 814            version = SaslMechanism.Version.SASL;
 815        } else if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
 816            challenge = success.findChildContent("additional-data");
 817            version = SaslMechanism.Version.SASL_2;
 818        } else {
 819            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 820        }
 821        try {
 822            currentLoginInfo.success(challenge, sslSocketOrNull(socket));
 823        } catch (final SaslMechanism.AuthenticationException e) {
 824            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e);
 825            throw new StateChangingException(Account.State.UNAUTHORIZED);
 826        }
 827        Log.d(
 828                Config.LOGTAG,
 829                account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
 830        if (SaslMechanism.pin(currentSaslMechanism)) {
 831            account.setPinnedMechanism(currentSaslMechanism);
 832        }
 833        if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
 834            final var authorizationJid = success.getAuthorizationIdentifier();
 835            checkAssignedDomainOrThrow(authorizationJid);
 836            Log.d(
 837                    Config.LOGTAG,
 838                    account.getJid().asBareJid()
 839                            + ": SASL 2.0 authorization identifier was "
 840                            + authorizationJid);
 841            // TODO this should only happen when we used Bind 2
 842            if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
 843                Log.d(
 844                        Config.LOGTAG,
 845                        account.getJid().asBareJid()
 846                                + ": jid changed during SASL 2.0. updating database");
 847            }
 848            final Bound bound = success.getExtension(Bound.class);
 849            final Resumed resumed = success.getExtension(Resumed.class);
 850            final Failed failed = success.getExtension(Failed.class);
 851            final Element tokenWrapper = success.findChild("token", Namespace.FAST);
 852            final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
 853            if (bound != null && resumed != null) {
 854                Log.d(
 855                        Config.LOGTAG,
 856                        account.getJid().asBareJid()
 857                                + ": server sent bound and resumed in SASL2 success");
 858                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
 859            }
 860            if (resumed != null && streamId != null) {
 861                if (this.boundStreamFeatures != null) {
 862                    this.streamFeatures = this.boundStreamFeatures;
 863                    Log.d(
 864                            Config.LOGTAG,
 865                            "putting previous stream features back in place: "
 866                                    + XmlHelper.printElementNames(this.boundStreamFeatures));
 867                }
 868                processResumed(resumed);
 869            } else if (failed != null) {
 870                processFailed(failed, false); // wait for new stream features
 871            }
 872            if (bound != null) {
 873                clearIqCallbacks();
 874                this.isBound = true;
 875                processNopStreamFeatures();
 876                this.boundStreamFeatures = this.streamFeatures;
 877                final Enabled streamManagementEnabled = bound.getExtension(Enabled.class);
 878                final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
 879                final boolean waitForDisco;
 880                if (streamManagementEnabled != null) {
 881                    resetOutboundStanzaQueue();
 882                    processEnabled(streamManagementEnabled);
 883                    waitForDisco = true;
 884                } else {
 885                    // if we did not enable stream management in bind do it now
 886                    waitForDisco = enableStreamManagement();
 887                }
 888                final boolean negotiatedCarbons;
 889                if (carbonsEnabled != null) {
 890                    negotiatedCarbons = true;
 891                    Log.d(
 892                            Config.LOGTAG,
 893                            account.getJid().asBareJid()
 894                                    + ": successfully enabled carbons (via Bind 2.0)");
 895                    features.carbonsEnabled = true;
 896                } else if (currentLoginInfo.inlineBindFeatures != null
 897                        && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
 898                    negotiatedCarbons = true;
 899                    Log.d(
 900                            Config.LOGTAG,
 901                            account.getJid().asBareJid()
 902                                    + ": successfully enabled carbons (via Bind 2.0/implicit)");
 903                    features.carbonsEnabled = true;
 904                } else {
 905                    negotiatedCarbons = false;
 906                }
 907                sendPostBindInitialization(waitForDisco, negotiatedCarbons);
 908            }
 909            final HashedToken.Mechanism tokenMechanism;
 910            if (SaslMechanism.hashedToken(currentSaslMechanism)) {
 911                tokenMechanism = ((HashedToken) currentSaslMechanism).getTokenMechanism();
 912            } else if (this.hashTokenRequest != null) {
 913                tokenMechanism = this.hashTokenRequest;
 914            } else {
 915                tokenMechanism = null;
 916            }
 917            if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
 918                if (ChannelBinding.priority(tokenMechanism.channelBinding)
 919                        >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
 920                    this.account.setFastToken(tokenMechanism, token);
 921                    Log.d(
 922                            Config.LOGTAG,
 923                            account.getJid().asBareJid()
 924                                    + ": storing hashed token "
 925                                    + tokenMechanism);
 926                } else {
 927                    Log.d(
 928                            Config.LOGTAG,
 929                            account.getJid().asBareJid()
 930                                    + ": not accepting hashed token "
 931                                    + tokenMechanism.name()
 932                                    + " for log in mechanism "
 933                                    + currentSaslMechanism.getMechanism());
 934                    this.account.resetFastToken();
 935                }
 936            } else if (this.hashTokenRequest != null) {
 937                Log.w(
 938                        Config.LOGTAG,
 939                        account.getJid().asBareJid()
 940                                + ": no response to our hashed token request "
 941                                + this.hashTokenRequest);
 942            }
 943        }
 944        mXmppConnectionService.databaseBackend.updateAccount(account);
 945        this.quickStartInProgress = false;
 946        if (version == SaslMechanism.Version.SASL) {
 947            tagReader.reset();
 948            sendStartStream(false, true);
 949            final Tag tag = tagReader.readTag();
 950            if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
 951                processStream();
 952            } else {
 953                throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
 954            }
 955        }
 956    }
 957
 958    private void resetOutboundStanzaQueue() {
 959        synchronized (this.mStanzaQueue) {
 960            final ImmutableList.Builder<Stanza> intermediateStanzasBuilder =
 961                    new ImmutableList.Builder<>();
 962            if (Config.EXTENDED_SM_LOGGING) {
 963                Log.d(
 964                        Config.LOGTAG,
 965                        account.getJid().asBareJid()
 966                                + ": stanzas sent before auth: "
 967                                + this.stanzasSentBeforeAuthentication);
 968            }
 969            for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
 970                final Stanza stanza = this.mStanzaQueue.get(i);
 971                if (stanza != null) {
 972                    intermediateStanzasBuilder.add(stanza);
 973                }
 974            }
 975            this.mStanzaQueue.clear();
 976            final var intermediateStanzas = intermediateStanzasBuilder.build();
 977            for (int i = 0; i < intermediateStanzas.size(); ++i) {
 978                this.mStanzaQueue.append(i + 1, intermediateStanzas.get(i));
 979            }
 980            this.stanzasSent = intermediateStanzas.size();
 981            if (Config.EXTENDED_SM_LOGGING) {
 982                Log.d(
 983                        Config.LOGTAG,
 984                        account.getJid().asBareJid()
 985                                + ": resetting outbound stanza queue to "
 986                                + this.stanzasSent);
 987            }
 988        }
 989    }
 990
 991    private void processNopStreamFeatures() throws IOException {
 992        final Tag tag = tagReader.readTag();
 993        if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
 994            this.streamFeatures =
 995                    tagReader.readElement(
 996                            tag, im.conversations.android.xmpp.model.streams.Features.class);
 997            Log.d(
 998                    Config.LOGTAG,
 999                    account.getJid().asBareJid()
1000                            + ": processed NOP stream features after success: "
1001                            + XmlHelper.printElementNames(this.streamFeatures));
1002        } else {
1003            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received " + tag);
1004            Log.d(
1005                    Config.LOGTAG,
1006                    account.getJid().asBareJid()
1007                            + ": server did not send stream features after SASL2 success");
1008            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1009        }
1010    }
1011
1012    private void processFailure(final AuthenticationFailure failure) throws IOException {
1013        final SaslMechanism.Version version;
1014        try {
1015            version = SaslMechanism.Version.of(failure);
1016        } catch (final IllegalArgumentException e) {
1017            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1018        }
1019
1020        final LoginInfo currentLoginInfo = this.loginInfo;
1021        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
1022            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1023        }
1024
1025        Log.d(Config.LOGTAG, failure.toString());
1026        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
1027        if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) {
1028            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
1029            account.resetFastToken();
1030            mXmppConnectionService.databaseBackend.updateAccount(account);
1031        }
1032        final var errorCondition = failure.getErrorCondition();
1033        if (errorCondition instanceof SaslError.InvalidMechanism
1034                || errorCondition instanceof SaslError.MechanismTooWeak) {
1035            Log.d(
1036                    Config.LOGTAG,
1037                    account.getJid().asBareJid()
1038                            + ": invalid or too weak mechanism. resetting quick start");
1039            if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) {
1040                mXmppConnectionService.databaseBackend.updateAccount(account);
1041            }
1042            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1043        } else if (errorCondition instanceof SaslError.TemporaryAuthFailure) {
1044            throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
1045        } else if (errorCondition instanceof SaslError.AccountDisabled) {
1046            final String text = failure.getText();
1047            if (Strings.isNullOrEmpty(text)) {
1048                throw new StateChangingException(Account.State.UNAUTHORIZED);
1049            }
1050            final Matcher matcher = Patterns.URI_HTTP.matcher(text);
1051            if (matcher.find()) {
1052                final HttpUrl url;
1053                try {
1054                    url = HttpUrl.get(text.substring(matcher.start(), matcher.end()));
1055                } catch (final IllegalArgumentException e) {
1056                    throw new StateChangingException(Account.State.UNAUTHORIZED);
1057                }
1058                if (url.isHttps()) {
1059                    this.redirectionUrl = url;
1060                    throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
1061                }
1062            }
1063        }
1064        if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) {
1065            Log.d(
1066                    Config.LOGTAG,
1067                    account.getJid().asBareJid()
1068                            + ": fast authentication failed. falling back to regular"
1069                            + " authentication");
1070            this.loginInfo = null;
1071            authenticate();
1072        } else {
1073            throw new StateChangingException(Account.State.UNAUTHORIZED);
1074        }
1075    }
1076
1077    private static SSLSocket sslSocketOrNull(final Socket socket) {
1078        if (socket instanceof SSLSocket) {
1079            return (SSLSocket) socket;
1080        } else {
1081            return null;
1082        }
1083    }
1084
1085    private void processEnabled(final Enabled enabled) {
1086        final StreamId streamId = getStreamId(enabled);
1087        if (streamId == null) {
1088            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
1089        } else {
1090            Log.d(
1091                    Config.LOGTAG,
1092                    account.getJid().asBareJid()
1093                            + ": stream management enabled. resume at: "
1094                            + streamId.location);
1095        }
1096        this.streamId = streamId;
1097        this.stanzasReceived = 0;
1098        this.inSmacksSession = true;
1099        final var r = new Request();
1100        tagWriter.writeStanzaAsync(r);
1101    }
1102
1103    @Nullable
1104    private StreamId getStreamId(final Enabled enabled) {
1105        final Optional<String> id = enabled.getResumeId();
1106        final String locationAttribute = enabled.getLocation();
1107        final Resolver.Result currentResolverResult = this.currentResolverResult;
1108        final Resolver.Result location;
1109        if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
1110            location = null;
1111        } else {
1112            location = currentResolverResult.seeOtherHost(locationAttribute);
1113        }
1114        return id.isPresent() ? new StreamId(id.get(), location) : null;
1115    }
1116
1117    private void processResumed(final Resumed resumed) throws StateChangingException {
1118        final var pendingResumeId = this.pendingResumeId.pop();
1119        final var prevId = resumed.getPrevId();
1120        if (prevId == null || !prevId.equals(pendingResumeId)) {
1121            Log.d(
1122                    Config.LOGTAG,
1123                    account.getJid().asBareJid()
1124                            + ": server tried resume with unknown id "
1125                            + prevId);
1126            resetStreamId();
1127            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1128        }
1129        this.inSmacksSession = true;
1130        this.isBound = true;
1131        this.tagWriter.writeStanzaAsync(new Request());
1132        lastPacketReceived = SystemClock.elapsedRealtime();
1133        final Optional<Integer> h = resumed.getHandled();
1134        final int serverCount;
1135        if (h.isPresent()) {
1136            serverCount = h.get();
1137        } else {
1138            resetStreamId();
1139            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1140        }
1141        final ArrayList<Stanza> failedStanzas = new ArrayList<>();
1142        final boolean acknowledgedMessages;
1143        synchronized (this.mStanzaQueue) {
1144            if (serverCount < stanzasSent) {
1145                Log.d(
1146                        Config.LOGTAG,
1147                        account.getJid().asBareJid() + ": session resumed with lost packages");
1148                stanzasSent = serverCount;
1149            } else {
1150                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
1151            }
1152            acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
1153            for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
1154                failedStanzas.add(mStanzaQueue.valueAt(i));
1155            }
1156            mStanzaQueue.clear();
1157        }
1158        if (acknowledgedMessages) {
1159            mXmppConnectionService.updateConversationUi();
1160        }
1161        Log.d(
1162                Config.LOGTAG,
1163                account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
1164        for (final Stanza packet : failedStanzas) {
1165            if (packet instanceof im.conversations.android.xmpp.model.stanza.Message message) {
1166                mXmppConnectionService.markMessage(
1167                        account,
1168                        message.getTo().asBareJid(),
1169                        message.getId(),
1170                        Message.STATUS_UNSEND);
1171            }
1172            sendPacket(packet);
1173        }
1174        if (mWaitForDisco.get()) {
1175            this.lastDiscoStarted = SystemClock.elapsedRealtime();
1176            Log.d(
1177                    Config.LOGTAG,
1178                    account.getJid().asBareJid() + ": awaiting disco results after resume");
1179            changeStatus(Account.State.CONNECTING);
1180        } else {
1181            changeStatusToOnline();
1182        }
1183    }
1184
1185    private void changeStatusToOnline() {
1186        Log.d(
1187                Config.LOGTAG,
1188                account.getJid().asBareJid() + ": online with resource " + account.getResource());
1189        changeStatus(Account.State.ONLINE);
1190    }
1191
1192    private void processFailed(final Failed failed, final boolean sendBindRequest) {
1193        final Optional<Integer> serverCount = failed.getHandled();
1194        if (serverCount.isPresent()) {
1195            Log.d(
1196                    Config.LOGTAG,
1197                    account.getJid().asBareJid()
1198                            + ": resumption failed but server acknowledged stanza #"
1199                            + serverCount.get());
1200            final boolean acknowledgedMessages;
1201            synchronized (this.mStanzaQueue) {
1202                acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get());
1203            }
1204            if (acknowledgedMessages) {
1205                mXmppConnectionService.updateConversationUi();
1206            }
1207        } else {
1208            Log.d(
1209                    Config.LOGTAG,
1210                    account.getJid().asBareJid()
1211                            + ": resumption failed ("
1212                            + XmlHelper.print(failed.getChildren())
1213                            + ")");
1214        }
1215        resetStreamId();
1216        if (sendBindRequest) {
1217            sendBindRequest();
1218        }
1219    }
1220
1221    private boolean acknowledgeStanzaUpTo(final int serverCount) {
1222        if (serverCount > stanzasSent) {
1223            Log.e(
1224                    Config.LOGTAG,
1225                    "server acknowledged more stanzas than we sent. serverCount="
1226                            + serverCount
1227                            + ", ourCount="
1228                            + stanzasSent);
1229        }
1230        boolean acknowledgedMessages = false;
1231        for (int i = 0; i < mStanzaQueue.size(); ++i) {
1232            if (serverCount >= mStanzaQueue.keyAt(i)) {
1233                if (Config.EXTENDED_SM_LOGGING) {
1234                    Log.d(
1235                            Config.LOGTAG,
1236                            account.getJid().asBareJid()
1237                                    + ": server acknowledged stanza #"
1238                                    + mStanzaQueue.keyAt(i));
1239                }
1240                final Stanza stanza = mStanzaQueue.valueAt(i);
1241                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet
1242                        && acknowledgedListener != null) {
1243                    final String id = packet.getId();
1244                    final Jid to = packet.getTo();
1245                    if (id != null && to != null) {
1246                        acknowledgedMessages |=
1247                                acknowledgedListener.onMessageAcknowledged(account, to, id);
1248                    }
1249                }
1250                mStanzaQueue.removeAt(i);
1251                i--;
1252            }
1253        }
1254        return acknowledgedMessages;
1255    }
1256
1257    private <S extends Stanza> @NonNull S processPacket(final Tag currentTag, final Class<S> clazz)
1258            throws IOException {
1259        final S stanza = tagReader.readElement(currentTag, clazz);
1260        if (stanzasReceived == Integer.MAX_VALUE) {
1261            resetStreamId();
1262            throw new IOException("time to restart the session. cant handle >2 billion pcks");
1263        }
1264        if (inSmacksSession) {
1265            ++stanzasReceived;
1266        } else if (features.sm()) {
1267            Log.d(
1268                    Config.LOGTAG,
1269                    account.getJid().asBareJid()
1270                            + ": not counting stanza("
1271                            + stanza.getClass().getSimpleName()
1272                            + "). Not in smacks session.");
1273        }
1274        lastPacketReceived = SystemClock.elapsedRealtime();
1275        if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1276            Log.d(Config.LOGTAG, "[background stanza] " + stanza);
1277        }
1278        return stanza;
1279    }
1280
1281    private void processIq(final Tag currentTag) throws IOException {
1282        final Iq packet = processPacket(currentTag, Iq.class);
1283        if (packet.isInvalid()) {
1284            Log.e(
1285                    Config.LOGTAG,
1286                    "encountered invalid iq from='"
1287                            + packet.getFrom()
1288                            + "' to='"
1289                            + packet.getTo()
1290                            + "'");
1291            return;
1292        }
1293        if (Thread.currentThread().isInterrupted()) {
1294            Log.d(
1295                    Config.LOGTAG,
1296                    account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
1297            return;
1298        }
1299        if (packet.hasExtension(Jingle.class)
1300                && packet.getType() == Iq.Type.SET
1301                && isBound
1302                && LoginInfo.isSuccess(this.loginInfo)) {
1303            if (this.jingleListener != null) {
1304                this.jingleListener.onJinglePacketReceived(account, packet);
1305            }
1306        } else {
1307            final var callback = getIqPacketReceivedCallback(packet);
1308            if (callback == null) {
1309                Log.d(
1310                        Config.LOGTAG,
1311                        account.getJid().asBareJid().toString()
1312                                + ": no callback registered for IQ from "
1313                                + packet.getFrom());
1314                return;
1315            }
1316            try {
1317                callback.accept(packet);
1318            } catch (final StateChangingError error) {
1319                throw new StateChangingException(error.state);
1320            }
1321        }
1322    }
1323
1324    private Consumer<Iq> getIqPacketReceivedCallback(final Iq stanza)
1325            throws StateChangingException {
1326        final boolean isRequest =
1327                stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET;
1328        if (isRequest) {
1329            if (isBound && LoginInfo.isSuccess(this.loginInfo)) {
1330                return this.unregisteredIqListener;
1331            } else {
1332                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1333            }
1334        } else {
1335            synchronized (this.packetCallbacks) {
1336                final var pair = packetCallbacks.get(stanza.getId());
1337                if (pair == null) {
1338                    return null;
1339                }
1340                if (pair.first.toServer(account)) {
1341                    if (stanza.fromServer(account)) {
1342                        packetCallbacks.remove(stanza.getId());
1343                        return pair.second;
1344                    } else {
1345                        Log.e(
1346                                Config.LOGTAG,
1347                                account.getJid().asBareJid().toString()
1348                                        + ": ignoring spoofed iq packet");
1349                    }
1350                } else {
1351                    if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
1352                        packetCallbacks.remove(stanza.getId());
1353                        return pair.second;
1354                    } else {
1355                        Log.e(
1356                                Config.LOGTAG,
1357                                account.getJid().asBareJid().toString()
1358                                        + ": ignoring spoofed iq packet");
1359                    }
1360                }
1361            }
1362        }
1363        return null;
1364    }
1365
1366    private void processMessage(final Tag currentTag) throws IOException {
1367        final var packet =
1368                processPacket(currentTag, im.conversations.android.xmpp.model.stanza.Message.class);
1369        if (packet.isInvalid()) {
1370            Log.e(
1371                    Config.LOGTAG,
1372                    "encountered invalid message from='"
1373                            + packet.getFrom()
1374                            + "' to='"
1375                            + packet.getTo()
1376                            + "'");
1377            return;
1378        }
1379        if (Thread.currentThread().isInterrupted()) {
1380            Log.d(
1381                    Config.LOGTAG,
1382                    account.getJid().asBareJid()
1383                            + "Not processing message. Thread was interrupted");
1384            return;
1385        }
1386        this.messageListener.accept(packet);
1387    }
1388
1389    private void processPresence(final Tag currentTag) throws IOException {
1390        final var packet = processPacket(currentTag, Presence.class);
1391        if (packet.isInvalid()) {
1392            Log.e(
1393                    Config.LOGTAG,
1394                    "encountered invalid presence from='"
1395                            + packet.getFrom()
1396                            + "' to='"
1397                            + packet.getTo()
1398                            + "'");
1399            return;
1400        }
1401        if (Thread.currentThread().isInterrupted()) {
1402            Log.d(
1403                    Config.LOGTAG,
1404                    account.getJid().asBareJid()
1405                            + "Not processing presence. Thread was interrupted");
1406            return;
1407        }
1408        this.presenceListener.accept(packet);
1409    }
1410
1411    private void sendStartTLS() throws IOException {
1412        tagWriter.writeElement(new StartTls());
1413    }
1414
1415    private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
1416        tagReader.readElement(currentTag, Proceed.class);
1417        final Socket socket = this.socket;
1418        final SSLSocket sslSocket = upgradeSocketToTls(socket);
1419        this.socket = sslSocket;
1420        this.tagReader.setInputStream(sslSocket.getInputStream());
1421        this.tagWriter.setOutputStream(sslSocket.getOutputStream());
1422        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1423        final boolean quickStart;
1424        try {
1425            quickStart = establishStream(SSLSockets.version(sslSocket));
1426        } catch (final InterruptedException e) {
1427            return;
1428        }
1429        if (quickStart) {
1430            this.quickStartInProgress = true;
1431        }
1432        features.encryptionEnabled = true;
1433        final Tag tag = tagReader.readTag();
1434        if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1435            SSLSockets.log(account, sslSocket);
1436            processStream();
1437        } else {
1438            throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1439        }
1440        sslSocket.close();
1441    }
1442
1443    private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1444        final SSLSocketFactory sslSocketFactory;
1445        try {
1446            sslSocketFactory = getSSLSocketFactory();
1447        } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1448            throw new StateChangingException(Account.State.TLS_ERROR);
1449        }
1450        final InetAddress address = socket.getInetAddress();
1451        final SSLSocket sslSocket =
1452                (SSLSocket)
1453                        sslSocketFactory.createSocket(
1454                                socket, address.getHostAddress(), socket.getPort(), true);
1455        SSLSockets.setSecurity(sslSocket);
1456        SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1457        SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1458        final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1459        try {
1460            if (!xmppDomainVerifier.verify(
1461                    account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1462                Log.d(
1463                        Config.LOGTAG,
1464                        account.getJid().asBareJid()
1465                                + ": TLS certificate domain verification failed");
1466                FileBackend.close(sslSocket);
1467                throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1468            }
1469        } catch (final SSLPeerUnverifiedException e) {
1470            FileBackend.close(sslSocket);
1471            throw new StateChangingException(Account.State.TLS_ERROR);
1472        }
1473        return sslSocket;
1474    }
1475
1476    private void processStreamFeatures(final Tag currentTag) throws IOException {
1477        final var streamFeatures =
1478                tagReader.readElement(
1479                        currentTag, im.conversations.android.xmpp.model.streams.Features.class);
1480        final boolean isSecure = isSecure();
1481        if (streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) {
1482            sendStartTLS();
1483            return;
1484        }
1485        if (isSecure) {
1486            processSecureStreamFeatures(streamFeatures);
1487        } else {
1488            Log.d(
1489                    Config.LOGTAG,
1490                    account.getJid().asBareJid()
1491                            + ": STARTTLS not available "
1492                            + XmlHelper.printElementNames(streamFeatures));
1493            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1494        }
1495    }
1496
1497    private void processSecureStreamFeatures(
1498            final im.conversations.android.xmpp.model.streams.Features streamFeatures)
1499            throws IOException {
1500        this.streamFeatures = streamFeatures;
1501        final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1502        if (this.quickStartInProgress) {
1503            if (streamFeatures.hasStreamFeature(Authentication.class)) {
1504                Log.d(
1505                        Config.LOGTAG,
1506                        account.getJid().asBareJid()
1507                                + ": quick start in progress. ignoring features: "
1508                                + XmlHelper.printElementNames(this.streamFeatures));
1509                if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
1510                    return;
1511                }
1512                if (isFastTokenAvailable(this.streamFeatures.getExtension(Authentication.class))) {
1513                    Log.d(
1514                            Config.LOGTAG,
1515                            account.getJid().asBareJid()
1516                                    + ": fast token available; resetting quick start");
1517                    account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1518                    mXmppConnectionService.databaseBackend.updateAccount(account);
1519                }
1520                return;
1521            }
1522            Log.d(
1523                    Config.LOGTAG,
1524                    account.getJid().asBareJid()
1525                            + ": server lost support for SASL 2. quick start not possible");
1526            this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1527            mXmppConnectionService.databaseBackend.updateAccount(account);
1528            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1529        }
1530        if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1531                && account.isOptionSet(Account.OPTION_REGISTER)) {
1532            register();
1533        } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1534                && account.isOptionSet(Account.OPTION_REGISTER)) {
1535            throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1536        } else if (streamFeatures.hasStreamFeature(Authentication.class)
1537                && shouldAuthenticate
1538                && this.loginInfo == null) {
1539            authenticate(SaslMechanism.Version.SASL_2);
1540        } else if (streamFeatures.hasStreamFeature(Mechanisms.class)
1541                && shouldAuthenticate
1542                && this.loginInfo == null) {
1543            authenticate(SaslMechanism.Version.SASL);
1544        } else if (streamFeatures.streamManagement()
1545                && LoginInfo.isSuccess(loginInfo)
1546                && streamId != null
1547                && !inSmacksSession) {
1548            if (Config.EXTENDED_SM_LOGGING) {
1549                Log.d(
1550                        Config.LOGTAG,
1551                        account.getJid().asBareJid()
1552                                + ": resuming after stanza #"
1553                                + stanzasReceived);
1554            }
1555            final var streamId = this.streamId.id;
1556            final var resume = new Resume(streamId, stanzasReceived);
1557            prepareForResume(streamId);
1558            this.tagWriter.writeStanzaAsync(resume);
1559        } else if (needsBinding) {
1560            if (this.streamFeatures.hasChild("bind", Namespace.BIND)
1561                    && LoginInfo.isSuccess(loginInfo)) {
1562                sendBindRequest();
1563            } else {
1564                Log.d(
1565                        Config.LOGTAG,
1566                        account.getJid().asBareJid()
1567                                + ": unable to find bind feature "
1568                                + XmlHelper.printElementNames(this.streamFeatures));
1569                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1570            }
1571        } else {
1572            Log.d(
1573                    Config.LOGTAG,
1574                    account.getJid().asBareJid()
1575                            + ": received NOP stream features: "
1576                            + XmlHelper.printElementNames(this.streamFeatures));
1577        }
1578    }
1579
1580    private void authenticate() throws IOException {
1581        final boolean isSecure = isSecure();
1582        if (isSecure && this.streamFeatures.hasStreamFeature(Authentication.class)) {
1583            authenticate(SaslMechanism.Version.SASL_2);
1584        } else if (isSecure && this.streamFeatures.hasStreamFeature(Mechanisms.class)) {
1585            authenticate(SaslMechanism.Version.SASL);
1586        } else {
1587            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1588        }
1589    }
1590
1591    private boolean isSecure() {
1592        return (features.encryptionEnabled && this.socket instanceof SSLSocket)
1593                || Config.ALLOW_NON_TLS_CONNECTIONS
1594                || account.isDirectToOnion();
1595    }
1596
1597    private void authenticate(final SaslMechanism.Version version) throws IOException {
1598        if (this.loginInfo != null) {
1599            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1600        }
1601        final AuthenticationStreamFeature authElement;
1602        if (version == SaslMechanism.Version.SASL) {
1603            authElement = this.streamFeatures.getExtension(Mechanisms.class);
1604        } else {
1605            authElement = this.streamFeatures.getExtension(Authentication.class);
1606        }
1607        final Collection<String> mechanisms = authElement.getMechanismNames();
1608        final var cbExtension = this.streamFeatures.getExtension(SaslChannelBinding.class);
1609        final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbExtension);
1610        final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1611        final SaslMechanism saslMechanism =
1612                factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1613        this.validate(saslMechanism, mechanisms);
1614        final DowngradeProtection downgradeProtection;
1615        if (cbExtension != null) {
1616            downgradeProtection =
1617                    new DowngradeProtection(mechanisms, cbExtension.getChannelBindingTypes());
1618        } else {
1619            downgradeProtection = new DowngradeProtection(mechanisms);
1620        }
1621        if (saslMechanism instanceof ScramMechanism scramMechanism) {
1622            scramMechanism.setDowngradeProtection(downgradeProtection);
1623        }
1624        final boolean quickStartAvailable;
1625        final String firstMessage =
1626                saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1627        final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
1628        final AuthenticationRequest authenticate;
1629        final LoginInfo loginInfo;
1630        if (version == SaslMechanism.Version.SASL) {
1631            authenticate = new Auth();
1632            if (!Strings.isNullOrEmpty(firstMessage)) {
1633                authenticate.setContent(firstMessage);
1634            }
1635            quickStartAvailable = false;
1636            loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
1637        } else if (version == SaslMechanism.Version.SASL_2) {
1638            final Authentication authentication = (Authentication) authElement;
1639            final var inline = authentication.getInline();
1640            final boolean sm = inline != null && inline.hasExtension(StreamManagement.class);
1641            final HashedToken.Mechanism hashTokenRequest;
1642            if (usingFast) {
1643                hashTokenRequest = null;
1644            } else if (inline != null) {
1645                hashTokenRequest =
1646                        HashedToken.Mechanism.best(
1647                                inline.getFastMechanisms(), SSLSockets.version(this.socket));
1648                // TODO warn or fail early if channel binding priority isn’t high enough compared to
1649                // login mechanism
1650                // ChannelBinding.priority(hashTokenRequest.channelBinding)
1651                //                        <
1652                // ChannelBindingMechanism.getPriority(saslMechanism)
1653            } else {
1654                hashTokenRequest = null;
1655            }
1656            final Collection<String> bindFeatures = Bind2.features(inline);
1657            quickStartAvailable =
1658                    sm
1659                            && bindFeatures != null
1660                            && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1661            if (bindFeatures != null) {
1662                try {
1663                    mXmppConnectionService.restoredFromDatabaseLatch.await();
1664                } catch (final InterruptedException e) {
1665                    Log.d(
1666                            Config.LOGTAG,
1667                            account.getJid().asBareJid()
1668                                    + ": interrupted while waiting for DB restore during SASL2"
1669                                    + " bind");
1670                    return;
1671                }
1672            }
1673            loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
1674            this.hashTokenRequest = hashTokenRequest;
1675            authenticate =
1676                    generateAuthenticationRequest(
1677                            firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1678        } else {
1679            throw new AssertionError("Missing implementation for " + version);
1680        }
1681        this.loginInfo = loginInfo;
1682        if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1683            mXmppConnectionService.databaseBackend.updateAccount(account);
1684        }
1685
1686        Log.d(
1687                Config.LOGTAG,
1688                account.getJid().toString()
1689                        + ": Authenticating with "
1690                        + version
1691                        + "/"
1692                        + LoginInfo.mechanism(loginInfo).getMechanism());
1693        authenticate.setMechanism(LoginInfo.mechanism(loginInfo));
1694        synchronized (this.mStanzaQueue) {
1695            this.stanzasSentBeforeAuthentication = this.stanzasSent;
1696            tagWriter.writeElement(authenticate);
1697        }
1698    }
1699
1700    private static boolean isFastTokenAvailable(final Authentication authentication) {
1701        final var inline = authentication == null ? null : authentication.getInline();
1702        return inline != null && inline.hasExtension(Fast.class);
1703    }
1704
1705    private void validate(
1706            final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms)
1707            throws StateChangingException {
1708        if (saslMechanism == null) {
1709            Log.d(
1710                    Config.LOGTAG,
1711                    account.getJid().asBareJid()
1712                            + ": unable to find supported SASL mechanism in "
1713                            + mechanisms);
1714            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1715        }
1716        checkRequireChannelBinding(saslMechanism);
1717        if (SaslMechanism.hashedToken(saslMechanism)) {
1718            return;
1719        }
1720        final int pinnedMechanism = account.getPinnedMechanismPriority();
1721        if (pinnedMechanism > saslMechanism.getPriority()) {
1722            Log.e(
1723                    Config.LOGTAG,
1724                    "Auth failed. Authentication mechanism "
1725                            + saslMechanism.getMechanism()
1726                            + " has lower priority ("
1727                            + saslMechanism.getPriority()
1728                            + ") than pinned priority ("
1729                            + pinnedMechanism
1730                            + "). Possible downgrade attack?");
1731            throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1732        }
1733    }
1734
1735    private void checkRequireChannelBinding(@NonNull final SaslMechanism mechanism)
1736            throws StateChangingException {
1737        if (appSettings.isRequireChannelBinding()) {
1738            if (mechanism instanceof ChannelBindingMechanism) {
1739                return;
1740            }
1741            Log.d(Config.LOGTAG, account.getJid() + ": server did not offer channel binding");
1742            throw new StateChangingException(Account.State.CHANNEL_BINDING);
1743        }
1744    }
1745
1746    private void checkAssignedDomainOrThrow(final Jid jid) throws StateChangingException {
1747        if (jid == null) {
1748            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": bind response is missing jid");
1749            throw new StateChangingException(Account.State.BIND_FAILURE);
1750        }
1751        final var current = this.account.getJid().getDomain();
1752        if (jid.getDomain().equals(current)) {
1753            return;
1754        }
1755        Log.d(
1756                Config.LOGTAG,
1757                account.getJid().asBareJid()
1758                        + ": server tried to re-assign domain to "
1759                        + jid.getDomain());
1760        throw new StateChangingException(Account.State.BIND_FAILURE);
1761    }
1762
1763    private void checkAssignedDomain(final Jid jid) {
1764        try {
1765            checkAssignedDomainOrThrow(jid);
1766        } catch (final StateChangingException e) {
1767            throw new StateChangingError(e.state);
1768        }
1769    }
1770
1771    private AuthenticationRequest generateAuthenticationRequest(
1772            final String firstMessage, final boolean usingFast) {
1773        return generateAuthenticationRequest(
1774                firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1775    }
1776
1777    private AuthenticationRequest generateAuthenticationRequest(
1778            final String firstMessage,
1779            final boolean usingFast,
1780            final HashedToken.Mechanism hashedTokenRequest,
1781            final Collection<String> bind,
1782            final boolean inlineStreamManagement) {
1783        final var authenticate = new Authenticate();
1784        if (!Strings.isNullOrEmpty(firstMessage)) {
1785            authenticate.addChild("initial-response").setContent(firstMessage);
1786        }
1787        final var userAgent =
1788                authenticate.addExtension(
1789                        new UserAgent(
1790                                AccountUtils.publicDeviceId(
1791                                        account, appSettings.getInstallationId())));
1792        userAgent.setSoftware(
1793                String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
1794        if (!PhoneHelper.isEmulator()) {
1795            userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1796        }
1797        // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
1798        // (because we would rather just do a normal SM/resume)
1799        final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1800        if (bind != null && mayAttemptBind) {
1801            authenticate.addChild(generateBindRequest(bind));
1802        }
1803        if (inlineStreamManagement && streamId != null) {
1804            final var streamId = this.streamId.id;
1805            final var resume = new Resume(streamId, stanzasReceived);
1806            prepareForResume(streamId);
1807            authenticate.addExtension(resume);
1808        }
1809        if (hashedTokenRequest != null) {
1810            authenticate.addExtension(new RequestToken(hashedTokenRequest));
1811        }
1812        if (usingFast) {
1813            authenticate.addExtension(new Fast());
1814        }
1815        return authenticate;
1816    }
1817
1818    private void prepareForResume(final String streamId) {
1819        this.mSmCatchupMessageCounter.set(0);
1820        this.mWaitingForSmCatchup.set(true);
1821        this.pendingResumeId.push(streamId);
1822    }
1823
1824    private Bind generateBindRequest(final Collection<String> bindFeatures) {
1825        Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1826        final var bind = new Bind();
1827        bind.setTag(BuildConfig.APP_NAME);
1828        if (bindFeatures.contains(Namespace.CARBONS)) {
1829            bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable());
1830        }
1831        if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1832            bind.addExtension(new Enable());
1833        }
1834        return bind;
1835    }
1836
1837    private void register() {
1838        final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1839        if (preAuth != null && features.invite()) {
1840            final Iq preAuthRequest = new Iq(Iq.Type.SET);
1841            preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1842            sendUnmodifiedIqPacket(
1843                    preAuthRequest,
1844                    (response) -> {
1845                        if (response.getType() == Iq.Type.RESULT) {
1846                            sendRegistryRequest();
1847                        } else {
1848                            final String error = response.getErrorCondition();
1849                            Log.d(
1850                                    Config.LOGTAG,
1851                                    account.getJid().asBareJid()
1852                                            + ": failed to pre auth. "
1853                                            + error);
1854                            throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1855                        }
1856                    },
1857                    true);
1858        } else {
1859            sendRegistryRequest();
1860        }
1861    }
1862
1863    private void sendRegistryRequest() {
1864        final Iq register = new Iq(Iq.Type.GET);
1865        register.query(Namespace.REGISTER);
1866        register.setTo(account.getDomain());
1867        sendUnmodifiedIqPacket(
1868                register,
1869                (packet) -> {
1870                    if (packet.getType() == Iq.Type.TIMEOUT) {
1871                        return;
1872                    }
1873                    if (packet.getType() == Iq.Type.ERROR) {
1874                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1875                    }
1876                    final Element query = packet.query(Namespace.REGISTER);
1877                    if (query.hasChild("username") && (query.hasChild("password"))) {
1878                        final Iq register1 = new Iq(Iq.Type.SET);
1879                        final Element username =
1880                                new Element("username").setContent(account.getUsername());
1881                        final Element password =
1882                                new Element("password").setContent(account.getPassword());
1883                        register1.query(Namespace.REGISTER).addChild(username);
1884                        register1.query().addChild(password);
1885                        register1.setFrom(account.getJid().asBareJid());
1886                        sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true);
1887                    } else if (query.hasChild("x", Namespace.DATA)) {
1888                        final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1889                        final Element blob = query.findChild("data", "urn:xmpp:bob");
1890                        final String id = packet.getId();
1891                        InputStream is;
1892                        if (blob != null) {
1893                            try {
1894                                final String base64Blob = blob.getContent();
1895                                final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1896                                is = new ByteArrayInputStream(strBlob);
1897                            } catch (Exception e) {
1898                                is = null;
1899                            }
1900                        } else {
1901                            final boolean useTor = this.appSettings.isUseTor() || account.isOnion();
1902                            try {
1903                                final String url = data.getValue("url");
1904                                final String fallbackUrl = data.getValue("captcha-fallback-url");
1905                                if (url != null) {
1906                                    is = HttpConnectionManager.open(url, useTor);
1907                                } else if (fallbackUrl != null) {
1908                                    is = HttpConnectionManager.open(fallbackUrl, useTor);
1909                                } else {
1910                                    is = null;
1911                                }
1912                            } catch (final IOException e) {
1913                                Log.d(
1914                                        Config.LOGTAG,
1915                                        account.getJid().asBareJid() + ": unable to fetch captcha",
1916                                        e);
1917                                is = null;
1918                            }
1919                        }
1920
1921                        if (is != null) {
1922                            Bitmap captcha = BitmapFactory.decodeStream(is);
1923                            try {
1924                                if (mXmppConnectionService.displayCaptchaRequest(
1925                                        account, id, data, captcha)) {
1926                                    return;
1927                                }
1928                            } catch (Exception e) {
1929                                throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1930                            }
1931                        }
1932                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1933                    } else if (query.hasChild("instructions")
1934                            || query.hasChild("x", Namespace.OOB)) {
1935                        final String instructions = query.findChildContent("instructions");
1936                        final Element oob = query.findChild("x", Namespace.OOB);
1937                        final String url = oob == null ? null : oob.findChildContent("url");
1938                        if (url != null) {
1939                            setAccountCreationFailed(url);
1940                        } else if (instructions != null) {
1941                            final Matcher matcher = Patterns.URI_HTTP.matcher(instructions);
1942                            if (matcher.find()) {
1943                                setAccountCreationFailed(
1944                                        instructions.substring(matcher.start(), matcher.end()));
1945                            }
1946                        }
1947                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1948                    }
1949                },
1950                true);
1951    }
1952
1953    public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
1954        final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
1955        this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
1956    }
1957
1958    private void processRegistrationResponse(final Iq response) {
1959        if (response.getType() == Iq.Type.RESULT) {
1960            account.setOption(Account.OPTION_REGISTER, false);
1961            Log.d(
1962                    Config.LOGTAG,
1963                    account.getJid().asBareJid()
1964                            + ": successfully registered new account on server");
1965            throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
1966        } else {
1967            final Account.State state = getRegistrationFailedState(response);
1968            throw new StateChangingError(state);
1969        }
1970    }
1971
1972    @NonNull
1973    private static Account.State getRegistrationFailedState(final Iq response) {
1974        final List<String> PASSWORD_TOO_WEAK_MESSAGES =
1975                Arrays.asList("The password is too weak", "Please use a longer password.");
1976        final var error = response.getError();
1977        final var condition = error == null ? null : error.getCondition();
1978        final Account.State state;
1979        if (condition instanceof Condition.Conflict) {
1980            state = Account.State.REGISTRATION_CONFLICT;
1981        } else if (condition instanceof Condition.ResourceConstraint) {
1982            state = Account.State.REGISTRATION_PLEASE_WAIT;
1983        } else if (condition instanceof Condition.NotAcceptable
1984                && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
1985            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
1986        } else {
1987            state = Account.State.REGISTRATION_FAILED;
1988        }
1989        return state;
1990    }
1991
1992    private void setAccountCreationFailed(final String url) {
1993        final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1994        if (httpUrl != null && httpUrl.isHttps()) {
1995            this.redirectionUrl = httpUrl;
1996            throw new StateChangingError(Account.State.REGISTRATION_WEB);
1997        }
1998        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1999    }
2000
2001    public HttpUrl getRedirectionUrl() {
2002        return this.redirectionUrl;
2003    }
2004
2005    public void resetEverything() {
2006        resetAttemptCount(true);
2007        resetStreamId();
2008        clearIqCallbacks();
2009        synchronized (this.mStanzaQueue) {
2010            this.stanzasSent = 0;
2011            this.mStanzaQueue.clear();
2012        }
2013        this.redirectionUrl = null;
2014        getManager(DiscoManager.class).clear();
2015        synchronized (this.commands) {
2016            this.commands.clear();
2017        }
2018        this.loginInfo = null;
2019    }
2020
2021    private void sendBindRequest() {
2022        try {
2023            mXmppConnectionService.restoredFromDatabaseLatch.await();
2024        } catch (InterruptedException e) {
2025            Log.d(
2026                    Config.LOGTAG,
2027                    account.getJid().asBareJid()
2028                            + ": interrupted while waiting for DB restore during bind");
2029            return;
2030        }
2031        clearIqCallbacks();
2032        if (account.getJid().isBareJid()) {
2033            account.setResource(createNewResource());
2034        } else {
2035            fixResource(mXmppConnectionService, account);
2036        }
2037        final Iq iq = new Iq(Iq.Type.SET);
2038        final String resource =
2039                Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND
2040                        ? CryptoHelper.random(9)
2041                        : account.getResource();
2042        iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource);
2043        this.sendUnmodifiedIqPacket(
2044                iq,
2045                (packet) -> {
2046                    if (packet.getType() == Iq.Type.TIMEOUT) {
2047                        return;
2048                    }
2049                    final var bind =
2050                            packet.getExtension(
2051                                    im.conversations.android.xmpp.model.bind.Bind.class);
2052                    if (bind != null && packet.getType() == Iq.Type.RESULT) {
2053                        isBound = true;
2054                        final Jid assignedJid = bind.getJid();
2055                        checkAssignedDomain(assignedJid);
2056                        if (account.setJid(assignedJid)) {
2057                            Log.d(
2058                                    Config.LOGTAG,
2059                                    account.getJid().asBareJid()
2060                                            + ": jid changed during bind. updating database");
2061                            mXmppConnectionService.databaseBackend.updateAccount(account);
2062                        }
2063                        if (streamFeatures.hasChild("session")
2064                                && !streamFeatures.findChild("session").hasChild("optional")) {
2065                            sendStartSession();
2066                        } else {
2067                            final boolean waitForDisco = enableStreamManagement();
2068                            sendPostBindInitialization(waitForDisco, false);
2069                        }
2070                    } else {
2071                        Log.d(
2072                                Config.LOGTAG,
2073                                account.getJid()
2074                                        + ": disconnecting because of bind failure ("
2075                                        + packet);
2076                        final var error = packet.getError();
2077                        // TODO error.is(Condition)
2078                        if (packet.getType() == Iq.Type.ERROR
2079                                && error != null
2080                                && error.hasChild("conflict")) {
2081                            account.setResource(createNewResource());
2082                        }
2083                        throw new StateChangingError(Account.State.BIND_FAILURE);
2084                    }
2085                },
2086                true);
2087    }
2088
2089    private void clearIqCallbacks() {
2090        final Iq failurePacket = new Iq(Iq.Type.TIMEOUT);
2091        final ArrayList<Consumer<Iq>> callbacks = new ArrayList<>();
2092        synchronized (this.packetCallbacks) {
2093            if (this.packetCallbacks.isEmpty()) {
2094                return;
2095            }
2096            Log.d(
2097                    Config.LOGTAG,
2098                    account.getJid().asBareJid()
2099                            + ": clearing "
2100                            + this.packetCallbacks.size()
2101                            + " iq callbacks");
2102            final var iterator = this.packetCallbacks.values().iterator();
2103            while (iterator.hasNext()) {
2104                final var entry = iterator.next();
2105                callbacks.add(entry.second);
2106                iterator.remove();
2107            }
2108        }
2109        for (final var callback : callbacks) {
2110            try {
2111                callback.accept(failurePacket);
2112            } catch (StateChangingError error) {
2113                Log.d(
2114                        Config.LOGTAG,
2115                        account.getJid().asBareJid()
2116                                + ": caught StateChangingError("
2117                                + error.state.toString()
2118                                + ") while clearing callbacks");
2119                // ignore
2120            }
2121        }
2122        Log.d(
2123                Config.LOGTAG,
2124                account.getJid().asBareJid()
2125                        + ": done clearing iq callbacks. "
2126                        + this.packetCallbacks.size()
2127                        + " left");
2128    }
2129
2130    public void sendDiscoTimeout() {
2131        if (mWaitForDisco.compareAndSet(true, false)) {
2132            Log.d(
2133                    Config.LOGTAG,
2134                    account.getJid().asBareJid() + ": finalizing bind after disco timeout");
2135            finalizeBind();
2136        }
2137    }
2138
2139    private void sendStartSession() {
2140        Log.d(
2141                Config.LOGTAG,
2142                account.getJid().asBareJid() + ": sending legacy session to outdated server");
2143        final Iq startSession = new Iq(Iq.Type.SET);
2144        startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
2145        this.sendUnmodifiedIqPacket(
2146                startSession,
2147                (packet) -> {
2148                    if (packet.getType() == Iq.Type.RESULT) {
2149                        final boolean waitForDisco = enableStreamManagement();
2150                        sendPostBindInitialization(waitForDisco, false);
2151                    } else if (packet.getType() != Iq.Type.TIMEOUT) {
2152                        throw new StateChangingError(Account.State.SESSION_FAILURE);
2153                    }
2154                },
2155                true);
2156    }
2157
2158    private boolean enableStreamManagement() {
2159        final boolean streamManagement = this.streamFeatures.streamManagement();
2160        if (streamManagement) {
2161            synchronized (this.mStanzaQueue) {
2162                final var enable = new Enable();
2163                tagWriter.writeStanzaAsync(enable);
2164                stanzasSent = 0;
2165                mStanzaQueue.clear();
2166            }
2167            return true;
2168        } else {
2169            return false;
2170        }
2171    }
2172
2173    private void sendPostBindInitialization(
2174            final boolean waitForDisco, final boolean carbonsEnabled) {
2175        features.carbonsEnabled = carbonsEnabled;
2176        features.blockListRequested = false;
2177        getManager(DiscoManager.class).clear();
2178        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
2179        mWaitForDisco.set(waitForDisco);
2180        this.lastDiscoStarted = SystemClock.elapsedRealtime();
2181        mXmppConnectionService.scheduleWakeUpCall(
2182                Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
2183
2184        final var nodeHash = streamFeatures.getCapabilities();
2185        final var serverInfoFuture =
2186                getManager(DiscoManager.class)
2187                        .infoOrCache(Entity.discoItem(account.getDomain()), nodeHash);
2188
2189        final var features = getFeatures();
2190        if (!features.bind2()) {
2191            discoverMamPreferences();
2192        }
2193
2194        final var accountInfoFuture =
2195                getManager(DiscoManager.class)
2196                        .info(Entity.discoItem(account.getJid().asBareJid()), null);
2197
2198        final var itemsFuture =
2199                getManager(DiscoManager.class).itemsWithInfo(Entity.discoItem(account.getDomain()));
2200
2201        final var catchingServerFuture =
2202                Futures.catching(
2203                        serverInfoFuture,
2204                        DiscoManager.CapsHashMismatchException.class,
2205                        input -> {
2206                            Log.d(
2207                                    Config.LOGTAG,
2208                                    account.getJid().asBareJid() + ": error in server caps",
2209                                    input);
2210                            return null;
2211                        },
2212                        MoreExecutors.directExecutor());
2213
2214        Futures.addCallback(
2215                Futures.allAsList(accountInfoFuture, catchingServerFuture),
2216                new FutureCallback<>() {
2217                    @Override
2218                    public void onSuccess(List<Object> result) {
2219                        Log.d(
2220                                Config.LOGTAG,
2221                                account.getJid().asBareJid() + ": advanced stream future done");
2222                        enableAdvancedStreamFeatures();
2223                    }
2224
2225                    @Override
2226                    public void onFailure(@Nullable Throwable throwable) {
2227                        Log.d(
2228                                Config.LOGTAG,
2229                                "could not fetch disco for advanced stream features",
2230                                throwable);
2231                    }
2232                },
2233                MoreExecutors.directExecutor());
2234
2235        if (mWaitForDisco.get()) {
2236            final ListenableFuture<Void> discoComplete =
2237                    Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture)
2238                            .call(() -> null, MoreExecutors.directExecutor());
2239            Futures.addCallback(
2240                    discoComplete,
2241                    new FutureCallback<>() {
2242                        @Override
2243                        public void onSuccess(Void result) {
2244                            if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) {
2245                                Log.d(
2246                                        Config.LOGTAG,
2247                                        account.getJid().asBareJid()
2248                                                + ": reached timeout while waiting for disco");
2249                                return;
2250                            }
2251                            if (mWaitForDisco.compareAndSet(true, false)) {
2252                                finalizeBindOrError();
2253                            } else {
2254                                Log.d(
2255                                        Config.LOGTAG,
2256                                        account.getJid().asBareJid()
2257                                                + ": disco complete but bind was already"
2258                                                + " finalized");
2259                            }
2260                        }
2261
2262                        @Override
2263                        public void onFailure(@NonNull Throwable t) {
2264                            Log.d(Config.LOGTAG, "error in disco: ", t);
2265                        }
2266                    },
2267                    MoreExecutors.directExecutor());
2268        } else {
2269            finalizeBind();
2270        }
2271
2272        if (!mWaitForDisco.get()) {
2273            finalizeBind();
2274        }
2275        this.lastSessionStarted = SystemClock.elapsedRealtime();
2276    }
2277
2278    private boolean timeout(final ListenableFuture<?>... futures) {
2279        for (final ListenableFuture<?> future : futures) {
2280            if (future.isDone()) {
2281                try {
2282                    future.get();
2283                } catch (final Exception e) {
2284                    if (Throwables.getRootCause(e) instanceof TimeoutException) {
2285                        return true;
2286                    }
2287                }
2288            }
2289        }
2290        return false;
2291    }
2292
2293    private void discoverMamPreferences() {
2294        final Iq request = new Iq(Iq.Type.GET);
2295        request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2296        sendIqPacket(
2297                request,
2298                (response) -> {
2299                    if (response.getType() == Iq.Type.RESULT) {
2300                        Element prefs =
2301                                response.findChild(
2302                                        "prefs", MessageArchiveService.Version.MAM_2.namespace);
2303                        isMamPreferenceAlways =
2304                                "always"
2305                                        .equals(
2306                                                prefs == null
2307                                                        ? null
2308                                                        : prefs.getAttribute("default"));
2309                    }
2310                });
2311    }
2312
2313    private void discoverCommands() {
2314        final var future =
2315                getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
2316        Futures.addCallback(
2317                future,
2318                new FutureCallback<>() {
2319                    @Override
2320                    public void onSuccess(Map<String, Jid> result) {
2321                        synchronized (XmppConnection.this.commands) {
2322                            XmppConnection.this.commands.clear();
2323                            XmppConnection.this.commands.putAll(result);
2324                        }
2325                    }
2326
2327                    @Override
2328                    public void onFailure(@NonNull Throwable throwable) {
2329                        Log.d(
2330                                Config.LOGTAG,
2331                                account.getJid().asBareJid() + ": could not fetch commands",
2332                                throwable);
2333                    }
2334                },
2335                MoreExecutors.directExecutor());
2336    }
2337
2338    public boolean isMamPreferenceAlways() {
2339        return isMamPreferenceAlways;
2340    }
2341
2342    private void finalizeBindOrError() {
2343        try {
2344            finalizeBind();
2345        } catch (final Exception e) {
2346            throw new Error(e);
2347        }
2348    }
2349
2350    private void finalizeBind() {
2351        this.offlineMessagesRetrieved = false;
2352        this.bindListener.run();
2353        this.changeStatusToOnline();
2354    }
2355
2356    private void enableAdvancedStreamFeatures() {
2357        if (getFeatures().blocking() && !features.blockListRequested) {
2358            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2359            this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
2360        }
2361        for (final OnAdvancedStreamFeaturesLoaded listener :
2362                advancedStreamFeaturesLoadedListeners) {
2363            listener.onAdvancedStreamFeaturesAvailable(account);
2364        }
2365        if (getFeatures().carbons() && !features.carbonsEnabled) {
2366            sendEnableCarbons();
2367        }
2368        if (getFeatures().commands()) {
2369            discoverCommands();
2370        }
2371    }
2372
2373    private void sendEnableCarbons() {
2374        final Iq iq = new Iq(Iq.Type.SET);
2375        iq.addChild("enable", Namespace.CARBONS);
2376        this.sendIqPacket(
2377                iq,
2378                (packet) -> {
2379                    if (packet.getType() == Iq.Type.RESULT) {
2380                        Log.d(
2381                                Config.LOGTAG,
2382                                account.getJid().asBareJid() + ": successfully enabled carbons");
2383                        features.carbonsEnabled = true;
2384                    } else {
2385                        Log.d(
2386                                Config.LOGTAG,
2387                                account.getJid().asBareJid()
2388                                        + ": could not enable carbons "
2389                                        + packet);
2390                    }
2391                });
2392    }
2393
2394    private void processStreamError(final StreamError streamError) throws IOException {
2395        final var loginInfo = this.loginInfo;
2396        final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo);
2397        if (isSecureLoggedIn && streamError.hasChild("conflict")) {
2398            if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
2399                this.appSettings.resetInstallationId();
2400            }
2401            account.setResource(createNewResource());
2402            Log.d(
2403                    Config.LOGTAG,
2404                    account.getJid().asBareJid()
2405                            + ": switching resource due to conflict ("
2406                            + account.getResource()
2407                            + ")");
2408            throw new IOException("Closed stream due to resource conflict");
2409        } else if (streamError.hasChild("host-unknown")) {
2410            throw new StateChangingException(Account.State.HOST_UNKNOWN);
2411        } else if (streamError.hasChild("policy-violation")) {
2412            this.lastConnectionStarted = SystemClock.elapsedRealtime();
2413            final String text = streamError.findChildContent("text");
2414            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2415            if (isSecureLoggedIn) {
2416                failPendingMessages(text);
2417            }
2418            throw new StateChangingException(Account.State.POLICY_VIOLATION);
2419        } else if (streamError.hasChild("see-other-host")) {
2420            final String seeOtherHost = streamError.findChildContent("see-other-host");
2421            final Resolver.Result currentResolverResult = this.currentResolverResult;
2422            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2423                Log.d(
2424                        Config.LOGTAG,
2425                        account.getJid().asBareJid() + ": stream error " + streamError);
2426                throw new StateChangingException(Account.State.STREAM_ERROR);
2427            }
2428            Log.d(
2429                    Config.LOGTAG,
2430                    account.getJid().asBareJid()
2431                            + ": see other host: "
2432                            + seeOtherHost
2433                            + " "
2434                            + currentResolverResult);
2435            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2436            if (seeOtherResult != null) {
2437                this.seeOtherHostResolverResult = seeOtherResult;
2438                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2439            } else {
2440                throw new StateChangingException(Account.State.STREAM_ERROR);
2441            }
2442        } else {
2443            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2444            throw new StateChangingException(Account.State.STREAM_ERROR);
2445        }
2446    }
2447
2448    private void failPendingMessages(final String error) {
2449        synchronized (this.mStanzaQueue) {
2450            for (int i = 0; i < mStanzaQueue.size(); ++i) {
2451                final Stanza stanza = mStanzaQueue.valueAt(i);
2452                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) {
2453                    final String id = packet.getId();
2454                    final Jid to = packet.getTo();
2455                    mXmppConnectionService.markMessage(
2456                            account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2457                }
2458            }
2459        }
2460    }
2461
2462    private boolean establishStream(final SSLSockets.Version sslVersion)
2463            throws IOException, InterruptedException {
2464        final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2465        final SaslMechanism quickStartMechanism;
2466        if (secureConnection) {
2467            quickStartMechanism =
2468                    SaslMechanism.ensureAvailable(
2469                            account.getQuickStartMechanism(),
2470                            sslVersion,
2471                            appSettings.isRequireChannelBinding());
2472        } else {
2473            quickStartMechanism = null;
2474        }
2475        if (secureConnection
2476                && Config.QUICKSTART_ENABLED
2477                && quickStartMechanism != null
2478                && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2479            mXmppConnectionService.restoredFromDatabaseLatch.await();
2480            this.loginInfo =
2481                    new LoginInfo(
2482                            quickStartMechanism,
2483                            SaslMechanism.Version.SASL_2,
2484                            Bind2.QUICKSTART_FEATURES);
2485            final boolean usingFast = quickStartMechanism instanceof HashedToken;
2486            final AuthenticationRequest authenticate =
2487                    generateAuthenticationRequest(
2488                            quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
2489                            usingFast);
2490            authenticate.setMechanism(quickStartMechanism);
2491            sendStartStream(true, false);
2492            synchronized (this.mStanzaQueue) {
2493                this.stanzasSentBeforeAuthentication = this.stanzasSent;
2494                tagWriter.writeElement(authenticate);
2495            }
2496            Log.d(
2497                    Config.LOGTAG,
2498                    account.getJid().toString()
2499                            + ": quick start with "
2500                            + quickStartMechanism.getMechanism());
2501            return true;
2502        } else {
2503            sendStartStream(secureConnection, true);
2504            return false;
2505        }
2506    }
2507
2508    private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2509        final Tag stream = Tag.start("stream:stream");
2510        stream.setAttribute("to", account.getServer());
2511        if (from) {
2512            stream.setAttribute("from", account.getJid().asBareJid().toString());
2513        }
2514        stream.setAttribute("version", "1.0");
2515        stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2516        stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
2517        stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2518        tagWriter.writeTag(stream, flush);
2519    }
2520
2521    private static String createNewResource() {
2522        return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
2523    }
2524
2525    public ListenableFuture<Iq> sendIqPacket(final Iq request) {
2526        final SettableFuture<Iq> settable = SettableFuture.create();
2527        this.sendIqPacket(
2528                request,
2529                response -> {
2530                    final var type = response.getType();
2531                    switch (type) {
2532                        case RESULT -> settable.set(response);
2533                        case TIMEOUT -> settable.setException(new TimeoutException());
2534                        default -> settable.setException(new IqErrorResponseException(response));
2535                    }
2536                });
2537        return settable;
2538    }
2539
2540    public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
2541        packet.setFrom(account.getJid());
2542        return this.sendUnmodifiedIqPacket(packet, callback, false);
2543    }
2544
2545    public synchronized String sendUnmodifiedIqPacket(
2546            final Iq packet, final Consumer<Iq> callback, boolean force) {
2547        // TODO if callback != null verify that type is get or set
2548        if (packet.getId() == null) {
2549            packet.setId(CryptoHelper.random(9));
2550        }
2551        if (callback != null) {
2552            synchronized (this.packetCallbacks) {
2553                packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2554            }
2555        }
2556        this.sendPacket(packet, force);
2557        return packet.getId();
2558    }
2559
2560    public void sendResultFor(final Iq request, final Extension... extensions) {
2561        final var from = request.getFrom();
2562        final var id = request.getId();
2563        final var response = new Iq(Iq.Type.RESULT);
2564        response.setTo(from);
2565        response.setId(id);
2566        for (final Extension extension : extensions) {
2567            response.addExtension(extension);
2568        }
2569        this.sendPacket(response);
2570    }
2571
2572    public void sendErrorFor(
2573            final Iq request,
2574            final im.conversations.android.xmpp.model.error.Error.Type type,
2575            final Condition condition,
2576            final im.conversations.android.xmpp.model.error.Error.Extension... extensions) {
2577        final var from = request.getFrom();
2578        final var id = request.getId();
2579        final var response = new Iq(Iq.Type.ERROR);
2580        response.setTo(from);
2581        response.setId(id);
2582        final var error =
2583                response.addExtension(new im.conversations.android.xmpp.model.error.Error());
2584        error.setType(type);
2585        error.setCondition(condition);
2586        error.addExtensions(extensions);
2587        this.sendPacket(response);
2588    }
2589
2590    public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
2591        this.sendPacket(packet);
2592    }
2593
2594    public void sendPresencePacket(final Presence packet) {
2595        this.sendPacket(packet);
2596    }
2597
2598    private synchronized void sendPacket(final StreamElement packet) {
2599        sendPacket(packet, false);
2600    }
2601
2602    private synchronized void sendPacket(final StreamElement packet, final boolean force) {
2603        if (stanzasSent == Integer.MAX_VALUE) {
2604            resetStreamId();
2605            disconnect(true);
2606            return;
2607        }
2608        synchronized (this.mStanzaQueue) {
2609            if (force || isBound) {
2610                tagWriter.writeStanzaAsync(packet);
2611            } else {
2612                Log.d(
2613                        Config.LOGTAG,
2614                        account.getJid().asBareJid()
2615                                + " do not write stanza to unbound stream "
2616                                + packet.toString());
2617            }
2618            if (packet instanceof Stanza stanza) {
2619                if (this.mStanzaQueue.size() != 0) {
2620                    int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2621                    if (currentHighestKey != stanzasSent) {
2622                        throw new AssertionError("Stanza count messed up");
2623                    }
2624                }
2625
2626                ++stanzasSent;
2627                if (Config.EXTENDED_SM_LOGGING) {
2628                    Log.d(
2629                            Config.LOGTAG,
2630                            account.getJid().asBareJid()
2631                                    + ": counting outbound "
2632                                    + packet.getName()
2633                                    + " as #"
2634                                    + stanzasSent);
2635                }
2636                this.mStanzaQueue.append(stanzasSent, stanza);
2637                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message
2638                        && stanza.getId() != null
2639                        && inSmacksSession) {
2640                    if (Config.EXTENDED_SM_LOGGING) {
2641                        Log.d(
2642                                Config.LOGTAG,
2643                                account.getJid().asBareJid()
2644                                        + ": requesting ack for message stanza #"
2645                                        + stanzasSent);
2646                    }
2647                    tagWriter.writeStanzaAsync(new Request());
2648                }
2649            }
2650        }
2651    }
2652
2653    public void sendPing() {
2654        if (!r()) {
2655            final Iq iq = new Iq(Iq.Type.GET);
2656            iq.setFrom(account.getJid());
2657            iq.addChild("ping", Namespace.PING);
2658            this.sendIqPacket(iq, null);
2659        }
2660        this.lastPingSent = SystemClock.elapsedRealtime();
2661    }
2662
2663    public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2664        this.jingleListener = listener;
2665    }
2666
2667    public void setOnStatusChangedListener(final OnStatusChanged listener) {
2668        this.statusListener = listener;
2669    }
2670
2671    public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2672        this.acknowledgedListener = listener;
2673    }
2674
2675    public void addOnAdvancedStreamFeaturesAvailableListener(
2676            final OnAdvancedStreamFeaturesLoaded listener) {
2677        this.advancedStreamFeaturesLoadedListeners.add(listener);
2678    }
2679
2680    private void forceCloseSocket() {
2681        FileBackend.close(this.socket);
2682        FileBackend.close(this.tagReader);
2683    }
2684
2685    public void interrupt() {
2686        if (this.mThread != null) {
2687            this.mThread.interrupt();
2688        }
2689    }
2690
2691    public void disconnect(final boolean force) {
2692        interrupt();
2693        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2694        if (force) {
2695            forceCloseSocket();
2696        } else {
2697            final TagWriter currentTagWriter = this.tagWriter;
2698            if (currentTagWriter.isActive()) {
2699                currentTagWriter.finish();
2700                final Socket currentSocket = this.socket;
2701                final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2702                try {
2703                    currentTagWriter.await(1, TimeUnit.SECONDS);
2704                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2705                    currentTagWriter.writeTag(Tag.end("stream:stream"));
2706                    if (streamCountDownLatch != null) {
2707                        if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2708                            Log.d(
2709                                    Config.LOGTAG,
2710                                    account.getJid().asBareJid() + ": remote ended stream");
2711                        } else {
2712                            Log.d(
2713                                    Config.LOGTAG,
2714                                    account.getJid().asBareJid()
2715                                            + ": remote has not closed socket. force closing");
2716                        }
2717                    }
2718                } catch (InterruptedException e) {
2719                    Log.d(
2720                            Config.LOGTAG,
2721                            account.getJid().asBareJid()
2722                                    + ": interrupted while gracefully closing stream");
2723                } catch (final IOException e) {
2724                    Log.d(
2725                            Config.LOGTAG,
2726                            account.getJid().asBareJid()
2727                                    + ": io exception during disconnect ("
2728                                    + e.getMessage()
2729                                    + ")");
2730                } finally {
2731                    FileBackend.close(currentSocket);
2732                }
2733            } else {
2734                forceCloseSocket();
2735            }
2736        }
2737    }
2738
2739    private void resetStreamId() {
2740        this.pendingResumeId.clear();
2741        this.streamId = null;
2742        this.boundStreamFeatures = null;
2743    }
2744
2745    public <M extends AbstractManager> M getManager(final Class<M> clazz) {
2746        return this.managers.getInstance(clazz);
2747    }
2748
2749    private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
2750        final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
2751        for (final Entry<Jid, InfoQuery> cursor :
2752                getManager(DiscoManager.class).getServerItems().entrySet()) {
2753            if (cursor.getValue().getFeatureStrings().contains(feature)) {
2754                items.add(cursor);
2755            }
2756        }
2757        return items;
2758    }
2759
2760    public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
2761        return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
2762    }
2763
2764    public Jid findDiscoItemByFeature(final String feature) {
2765        final var items = findDiscoItemsByFeature(feature);
2766        if (items.isEmpty()) {
2767            return null;
2768        }
2769        return Iterables.getFirst(items, null).getKey();
2770    }
2771
2772    public boolean r() {
2773        if (getFeatures().sm()) {
2774            this.tagWriter.writeStanzaAsync(new Request());
2775            return true;
2776        } else {
2777            return false;
2778        }
2779    }
2780
2781    public List<String> getMucServersWithholdAccount() {
2782        final List<String> servers = getMucServers();
2783        servers.remove(account.getDomain().toString());
2784        return servers;
2785    }
2786
2787    public List<String> getMucServers() {
2788        List<String> servers = new ArrayList<>();
2789        for (final Entry<Jid, InfoQuery> entry :
2790                getManager(DiscoManager.class).getServerItems().entrySet()) {
2791            final var value = entry.getValue();
2792            if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
2793                    && value.hasIdentityWithCategoryAndType("conference", "text")
2794                    && !value.getFeatureStrings().contains("jabber:iq:gateway")
2795                    && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
2796                servers.add(entry.getKey().toString());
2797            }
2798        }
2799        return servers;
2800    }
2801
2802    public String getMucServer() {
2803        return Iterables.getFirst(getMucServers(), null);
2804    }
2805
2806    public int getTimeToNextAttempt(final boolean aggressive) {
2807        final int interval;
2808        if (aggressive) {
2809            interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2810        } else {
2811            final int additionalTime =
2812                    account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2813            interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2814        }
2815        final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000);
2816        return interval - connectionDuration;
2817    }
2818
2819    public int getAttempt() {
2820        return this.attempt;
2821    }
2822
2823    public Features getFeatures() {
2824        return this.features;
2825    }
2826
2827    public long getLastSessionEstablished() {
2828        final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2829        return System.currentTimeMillis() - diff;
2830    }
2831
2832    public long getConnectionDuration() {
2833        return SystemClock.elapsedRealtime() - this.lastConnectionStarted;
2834    }
2835
2836    public long getDiscoDuration() {
2837        return SystemClock.elapsedRealtime() - this.lastDiscoStarted;
2838    }
2839
2840    public long getLastPingSent() {
2841        return this.lastPingSent;
2842    }
2843
2844    public long getLastPacketReceived() {
2845        return this.lastPacketReceived;
2846    }
2847
2848    public void sendActive() {
2849        this.sendPacket(new Active());
2850    }
2851
2852    public void sendInactive() {
2853        this.sendPacket(new Inactive());
2854    }
2855
2856    public void resetAttemptCount(boolean resetConnectTime) {
2857        this.attempt = 0;
2858        if (resetConnectTime) {
2859            this.lastConnectionStarted = 0;
2860        }
2861    }
2862
2863    public void setInteractive(boolean interactive) {
2864        this.mInteractive = interactive;
2865    }
2866
2867    private IqGenerator getIqGenerator() {
2868        return mXmppConnectionService.getIqGenerator();
2869    }
2870
2871    public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
2872        if (trackOfflineMessageRetrieval) {
2873            final Iq iqPing = new Iq(Iq.Type.GET);
2874            iqPing.addChild("ping", Namespace.PING);
2875            this.sendIqPacket(
2876                    iqPing,
2877                    (response) -> {
2878                        Log.d(
2879                                Config.LOGTAG,
2880                                account.getJid().asBareJid()
2881                                        + ": got ping response after sending initial presence");
2882                        XmppConnection.this.offlineMessagesRetrieved = true;
2883                    });
2884        } else {
2885            this.offlineMessagesRetrieved = true;
2886        }
2887    }
2888
2889    public boolean isOfflineMessagesRetrieved() {
2890        return this.offlineMessagesRetrieved;
2891    }
2892
2893    public void fetchRoster() {
2894        final Iq iqPacket = new Iq(Iq.Type.GET);
2895        final var version = account.getRosterVersion();
2896        if (Strings.isNullOrEmpty(account.getRosterVersion())) {
2897            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
2898        } else {
2899            Log.d(
2900                    Config.LOGTAG,
2901                    account.getJid().asBareJid() + ": fetching roster version " + version);
2902        }
2903        iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
2904        sendIqPacket(iqPacket, unregisteredIqListener);
2905    }
2906
2907    public void triggerConnectionTimeout() {
2908        final var duration = getConnectionDuration();
2909        Log.d(
2910                Config.LOGTAG,
2911                account.getJid().asBareJid() + ": connection timeout after " + duration + "ms");
2912
2913        // last connection time gets reset so time to next attempt is calculated correctly
2914        this.lastConnectionStarted = SystemClock.elapsedRealtime();
2915
2916        // interrupt needs to be called before status change; otherwise we interrupt the newly
2917        // created thread
2918        this.interrupt();
2919        this.forceCloseSocket();
2920        this.changeStatus(Account.State.CONNECTION_TIMEOUT);
2921    }
2922
2923    public Account getAccount() {
2924        return this.account;
2925    }
2926
2927    private class MyKeyManager implements X509KeyManager {
2928        @Override
2929        public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2930            return account.getPrivateKeyAlias();
2931        }
2932
2933        @Override
2934        public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2935            return null;
2936        }
2937
2938        @Override
2939        public X509Certificate[] getCertificateChain(String alias) {
2940            Log.d(Config.LOGTAG, "getting certificate chain");
2941            try {
2942                return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2943            } catch (final Exception e) {
2944                Log.d(Config.LOGTAG, "could not get certificate chain", e);
2945                return new X509Certificate[0];
2946            }
2947        }
2948
2949        @Override
2950        public String[] getClientAliases(String s, Principal[] principals) {
2951            final String alias = account.getPrivateKeyAlias();
2952            return alias != null ? new String[] {alias} : new String[0];
2953        }
2954
2955        @Override
2956        public String[] getServerAliases(String s, Principal[] principals) {
2957            return new String[0];
2958        }
2959
2960        @Override
2961        public PrivateKey getPrivateKey(String alias) {
2962            try {
2963                return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2964            } catch (Exception e) {
2965                return null;
2966            }
2967        }
2968    }
2969
2970    private static class LoginInfo {
2971        public final SaslMechanism saslMechanism;
2972        public final SaslMechanism.Version saslVersion;
2973        public final List<String> inlineBindFeatures;
2974        public final AtomicBoolean success = new AtomicBoolean(false);
2975
2976        private LoginInfo(
2977                final SaslMechanism saslMechanism,
2978                final SaslMechanism.Version saslVersion,
2979                final Collection<String> inlineBindFeatures) {
2980            Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
2981            Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
2982            this.saslMechanism = saslMechanism;
2983            this.saslVersion = saslVersion;
2984            this.inlineBindFeatures =
2985                    inlineBindFeatures == null
2986                            ? Collections.emptyList()
2987                            : ImmutableList.copyOf(inlineBindFeatures);
2988        }
2989
2990        public static SaslMechanism mechanism(final LoginInfo loginInfo) {
2991            return loginInfo == null ? null : loginInfo.saslMechanism;
2992        }
2993
2994        public void success(final String challenge, final SSLSocket sslSocket)
2995                throws SaslMechanism.AuthenticationException {
2996            if (Thread.currentThread().isInterrupted()) {
2997                throw new SaslMechanism.AuthenticationException("Race condition during auth");
2998            }
2999            final var response = this.saslMechanism.getResponse(challenge, sslSocket);
3000            if (!Strings.isNullOrEmpty(response)) {
3001                throw new SaslMechanism.AuthenticationException(
3002                        "processing success yielded another response");
3003            }
3004            if (this.success.compareAndSet(false, true)) {
3005                return;
3006            }
3007            throw new SaslMechanism.AuthenticationException("Process 'success' twice");
3008        }
3009
3010        public static boolean isSuccess(final LoginInfo loginInfo) {
3011            return loginInfo != null && loginInfo.success.get();
3012        }
3013    }
3014
3015    private static class StreamId {
3016        public final String id;
3017        public final Resolver.Result location;
3018
3019        private StreamId(String id, Resolver.Result location) {
3020            this.id = id;
3021            this.location = location;
3022        }
3023
3024        @NonNull
3025        @Override
3026        public String toString() {
3027            return MoreObjects.toStringHelper(this)
3028                    .add("id", id)
3029                    .add("location", location)
3030                    .toString();
3031        }
3032    }
3033
3034    private static class StateChangingError extends Error {
3035        private final Account.State state;
3036
3037        public StateChangingError(Account.State state) {
3038            this.state = state;
3039        }
3040    }
3041
3042    private static class StateChangingException extends IOException {
3043        private final Account.State state;
3044
3045        public StateChangingException(Account.State state) {
3046            this.state = state;
3047        }
3048    }
3049
3050    public abstract static class Delegate {
3051
3052        protected final Context context;
3053        protected final XmppConnection connection;
3054
3055        protected Delegate(final Context context, final XmppConnection connection) {
3056            this.context = context;
3057            this.connection = connection;
3058        }
3059
3060        protected Account getAccount() {
3061            return connection.account;
3062        }
3063
3064        protected DatabaseBackend getDatabase() {
3065            return DatabaseBackend.getInstance(context);
3066        }
3067
3068        protected <T extends AbstractManager> T getManager(final Class<T> type) {
3069            return connection.getManager(type);
3070        }
3071    }
3072
3073    public class Features {
3074        XmppConnection connection;
3075        private boolean carbonsEnabled = false;
3076        private boolean encryptionEnabled = false;
3077        private boolean blockListRequested = false;
3078
3079        public Features(final XmppConnection connection) {
3080            this.connection = connection;
3081        }
3082
3083        private boolean hasDiscoFeature(final Jid server, final String feature) {
3084            final var infoQuery = getManager(DiscoManager.class).get(server);
3085            return infoQuery != null && infoQuery.getFeatureStrings().contains(feature);
3086        }
3087
3088        public boolean carbons() {
3089            return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
3090        }
3091
3092        public boolean commands() {
3093            return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
3094        }
3095
3096        public boolean easyOnboardingInvites() {
3097            synchronized (commands) {
3098                return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
3099            }
3100        }
3101
3102        public boolean bookmarksConversion() {
3103            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
3104                    && pepPublishOptions();
3105        }
3106
3107        public boolean blocking() {
3108            return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
3109        }
3110
3111        public boolean spamReporting() {
3112            return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
3113        }
3114
3115        public boolean flexibleOfflineMessageRetrieval() {
3116            return hasDiscoFeature(
3117                    account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
3118        }
3119
3120        public boolean register() {
3121            return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
3122        }
3123
3124        public boolean invite() {
3125            return connection.streamFeatures != null
3126                    && connection.streamFeatures.hasChild("register", Namespace.INVITE);
3127        }
3128
3129        public boolean sm() {
3130            return streamId != null
3131                    || (connection.streamFeatures != null
3132                            && connection.streamFeatures.streamManagement());
3133        }
3134
3135        public boolean csi() {
3136            return connection.streamFeatures != null
3137                    && connection.streamFeatures.clientStateIndication();
3138        }
3139
3140        public boolean pep() {
3141            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3142            return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep");
3143        }
3144
3145        public boolean pepPersistent() {
3146            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3147            return infoQuery != null
3148                    && infoQuery
3149                            .getFeatureStrings()
3150                            .contains("http://jabber.org/protocol/pubsub#persistent-items");
3151        }
3152
3153        public boolean bind2() {
3154            final var loginInfo = XmppConnection.this.loginInfo;
3155            return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty();
3156        }
3157
3158        public boolean sasl2() {
3159            final var loginInfo = XmppConnection.this.loginInfo;
3160            return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2;
3161        }
3162
3163        public String loginMechanism() {
3164            final var loginInfo = XmppConnection.this.loginInfo;
3165            return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism();
3166        }
3167
3168        public boolean pepPublishOptions() {
3169            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
3170        }
3171
3172        public boolean pepConfigNodeMax() {
3173            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX);
3174        }
3175
3176        public boolean pepOmemoWhitelisted() {
3177            return hasDiscoFeature(
3178                    account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
3179        }
3180
3181        public boolean mam() {
3182            return MessageArchiveService.Version.has(getAccountFeatures());
3183        }
3184
3185        public Collection<String> getAccountFeatures() {
3186            final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
3187            return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings();
3188        }
3189
3190        public boolean push() {
3191            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
3192                    || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
3193        }
3194
3195        public boolean rosterVersioning() {
3196            return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
3197        }
3198
3199        public void setBlockListRequested(boolean value) {
3200            this.blockListRequested = value;
3201        }
3202
3203        public HttpUrl getServiceOutageStatus() {
3204            final var disco = getManager(DiscoManager.class).get(account.getDomain());
3205            if (disco == null) {
3206                return null;
3207            }
3208            final var address =
3209                    disco.getServiceDiscoveryExtension(
3210                            Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses");
3211            if (Strings.isNullOrEmpty(address)) {
3212                return null;
3213            }
3214            return HttpUrl.parse(address);
3215        }
3216
3217        public boolean httpUpload(long fileSize) {
3218            if (Config.DISABLE_HTTP_UPLOAD) {
3219                return false;
3220            }
3221            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
3222            if (result == null) {
3223                return false;
3224            }
3225            final long maxSize;
3226            try {
3227                maxSize =
3228                        Long.parseLong(
3229                                result.getValue()
3230                                        .getServiceDiscoveryExtension(
3231                                                Namespace.HTTP_UPLOAD, "max-file-size"));
3232            } catch (final Exception e) {
3233                return true;
3234            }
3235            if (fileSize <= maxSize) {
3236                return true;
3237            } else {
3238                Log.d(
3239                        Config.LOGTAG,
3240                        account.getJid().asBareJid()
3241                                + ": http upload is not available for files with"
3242                                + " size "
3243                                + fileSize
3244                                + " (max is "
3245                                + maxSize
3246                                + ")");
3247                return false;
3248            }
3249        }
3250
3251        public long getMaxHttpUploadSize() {
3252            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
3253            if (result == null) {
3254                return -1;
3255            }
3256            try {
3257                return Long.parseLong(
3258                        result.getValue()
3259                                .getServiceDiscoveryExtension(
3260                                        Namespace.HTTP_UPLOAD, "max-file-size"));
3261            } catch (final Exception e) {
3262                return -1;
3263                // ignored
3264            }
3265        }
3266
3267        public boolean stanzaIds() {
3268            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3269        }
3270
3271        public boolean bookmarks2() {
3272            return pepPublishOptions()
3273                    && pepConfigNodeMax()
3274                    && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3275        }
3276
3277        public boolean externalServiceDiscovery() {
3278            return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3279        }
3280
3281        public boolean mds() {
3282            return pepPublishOptions()
3283                    && pepConfigNodeMax()
3284                    && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION;
3285        }
3286
3287        public boolean mdsServerAssist() {
3288            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
3289        }
3290    }
3291}