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