XmppConnection.java

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