XmppConnection.java

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