XmppConnection.java

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