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