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 = mXmppConnectionService.useTorToConnect();
 308            final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
 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.AUTOLINK_WEB_URL.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 =
1889                                    mXmppConnectionService.useTorToConnect() || account.isOnion();
1890                            try {
1891                                final String url = data.getValue("url");
1892                                final String fallbackUrl = data.getValue("captcha-fallback-url");
1893                                if (url != null) {
1894                                    is = HttpConnectionManager.open(url, useTor);
1895                                } else if (fallbackUrl != null) {
1896                                    is = HttpConnectionManager.open(fallbackUrl, useTor);
1897                                } else {
1898                                    is = null;
1899                                }
1900                            } catch (final IOException e) {
1901                                Log.d(
1902                                        Config.LOGTAG,
1903                                        account.getJid().asBareJid() + ": unable to fetch captcha",
1904                                        e);
1905                                is = null;
1906                            }
1907                        }
1908
1909                        if (is != null) {
1910                            Bitmap captcha = BitmapFactory.decodeStream(is);
1911                            try {
1912                                if (mXmppConnectionService.displayCaptchaRequest(
1913                                        account, id, data, captcha)) {
1914                                    return;
1915                                }
1916                            } catch (Exception e) {
1917                                throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1918                            }
1919                        }
1920                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1921                    } else if (query.hasChild("instructions")
1922                            || query.hasChild("x", Namespace.OOB)) {
1923                        final String instructions = query.findChildContent("instructions");
1924                        final Element oob = query.findChild("x", Namespace.OOB);
1925                        final String url = oob == null ? null : oob.findChildContent("url");
1926                        if (url != null) {
1927                            setAccountCreationFailed(url);
1928                        } else if (instructions != null) {
1929                            final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1930                            if (matcher.find()) {
1931                                setAccountCreationFailed(
1932                                        instructions.substring(matcher.start(), matcher.end()));
1933                            }
1934                        }
1935                        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1936                    }
1937                },
1938                true);
1939    }
1940
1941    public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
1942        final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
1943        this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
1944    }
1945
1946    private void processRegistrationResponse(final Iq response) {
1947        if (response.getType() == Iq.Type.RESULT) {
1948            account.setOption(Account.OPTION_REGISTER, false);
1949            Log.d(
1950                    Config.LOGTAG,
1951                    account.getJid().asBareJid()
1952                            + ": successfully registered new account on server");
1953            throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
1954        } else {
1955            final Account.State state = getRegistrationFailedState(response);
1956            throw new StateChangingError(state);
1957        }
1958    }
1959
1960    @NonNull
1961    private static Account.State getRegistrationFailedState(final Iq response) {
1962        final List<String> PASSWORD_TOO_WEAK_MESSAGES =
1963                Arrays.asList("The password is too weak", "Please use a longer password.");
1964        final var error = response.getError();
1965        final var condition = error == null ? null : error.getCondition();
1966        final Account.State state;
1967        if (condition instanceof Condition.Conflict) {
1968            state = Account.State.REGISTRATION_CONFLICT;
1969        } else if (condition instanceof Condition.ResourceConstraint) {
1970            state = Account.State.REGISTRATION_PLEASE_WAIT;
1971        } else if (condition instanceof Condition.NotAcceptable
1972                && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
1973            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
1974        } else {
1975            state = Account.State.REGISTRATION_FAILED;
1976        }
1977        return state;
1978    }
1979
1980    private void setAccountCreationFailed(final String url) {
1981        final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1982        if (httpUrl != null && httpUrl.isHttps()) {
1983            this.redirectionUrl = httpUrl;
1984            throw new StateChangingError(Account.State.REGISTRATION_WEB);
1985        }
1986        throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1987    }
1988
1989    public HttpUrl getRedirectionUrl() {
1990        return this.redirectionUrl;
1991    }
1992
1993    public void resetEverything() {
1994        resetAttemptCount(true);
1995        resetStreamId();
1996        clearIqCallbacks();
1997        synchronized (this.mStanzaQueue) {
1998            this.stanzasSent = 0;
1999            this.mStanzaQueue.clear();
2000        }
2001        this.redirectionUrl = null;
2002        synchronized (this.disco) {
2003            disco.clear();
2004        }
2005        synchronized (this.commands) {
2006            this.commands.clear();
2007        }
2008        this.loginInfo = null;
2009    }
2010
2011    private void sendBindRequest() {
2012        try {
2013            mXmppConnectionService.restoredFromDatabaseLatch.await();
2014        } catch (InterruptedException e) {
2015            Log.d(
2016                    Config.LOGTAG,
2017                    account.getJid().asBareJid()
2018                            + ": interrupted while waiting for DB restore during bind");
2019            return;
2020        }
2021        clearIqCallbacks();
2022        if (account.getJid().isBareJid()) {
2023            account.setResource(createNewResource());
2024        } else {
2025            fixResource(mXmppConnectionService, account);
2026        }
2027        final Iq iq = new Iq(Iq.Type.SET);
2028        final String resource =
2029                Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND
2030                        ? CryptoHelper.random(9)
2031                        : account.getResource();
2032        iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource);
2033        this.sendUnmodifiedIqPacket(
2034                iq,
2035                (packet) -> {
2036                    if (packet.getType() == Iq.Type.TIMEOUT) {
2037                        return;
2038                    }
2039                    final var bind =
2040                            packet.getExtension(
2041                                    im.conversations.android.xmpp.model.bind.Bind.class);
2042                    if (bind != null && packet.getType() == Iq.Type.RESULT) {
2043                        isBound = true;
2044                        final Jid assignedJid = bind.getJid();
2045                        checkAssignedDomain(assignedJid);
2046                        if (account.setJid(assignedJid)) {
2047                            Log.d(
2048                                    Config.LOGTAG,
2049                                    account.getJid().asBareJid()
2050                                            + ": jid changed during bind. updating database");
2051                            mXmppConnectionService.databaseBackend.updateAccount(account);
2052                        }
2053                        if (streamFeatures.hasChild("session")
2054                                && !streamFeatures.findChild("session").hasChild("optional")) {
2055                            sendStartSession();
2056                        } else {
2057                            final boolean waitForDisco = enableStreamManagement();
2058                            sendPostBindInitialization(waitForDisco, false);
2059                        }
2060                    } else {
2061                        Log.d(
2062                                Config.LOGTAG,
2063                                account.getJid()
2064                                        + ": disconnecting because of bind failure ("
2065                                        + packet);
2066                        final var error = packet.getError();
2067                        // TODO error.is(Condition)
2068                        if (packet.getType() == Iq.Type.ERROR
2069                                && error != null
2070                                && error.hasChild("conflict")) {
2071                            account.setResource(createNewResource());
2072                        }
2073                        throw new StateChangingError(Account.State.BIND_FAILURE);
2074                    }
2075                },
2076                true);
2077    }
2078
2079    private void clearIqCallbacks() {
2080        final Iq failurePacket = new Iq(Iq.Type.TIMEOUT);
2081        final ArrayList<Consumer<Iq>> callbacks = new ArrayList<>();
2082        synchronized (this.packetCallbacks) {
2083            if (this.packetCallbacks.isEmpty()) {
2084                return;
2085            }
2086            Log.d(
2087                    Config.LOGTAG,
2088                    account.getJid().asBareJid()
2089                            + ": clearing "
2090                            + this.packetCallbacks.size()
2091                            + " iq callbacks");
2092            final var iterator = this.packetCallbacks.values().iterator();
2093            while (iterator.hasNext()) {
2094                final var entry = iterator.next();
2095                callbacks.add(entry.second);
2096                iterator.remove();
2097            }
2098        }
2099        for (final var callback : callbacks) {
2100            try {
2101                callback.accept(failurePacket);
2102            } catch (StateChangingError error) {
2103                Log.d(
2104                        Config.LOGTAG,
2105                        account.getJid().asBareJid()
2106                                + ": caught StateChangingError("
2107                                + error.state.toString()
2108                                + ") while clearing callbacks");
2109                // ignore
2110            }
2111        }
2112        Log.d(
2113                Config.LOGTAG,
2114                account.getJid().asBareJid()
2115                        + ": done clearing iq callbacks. "
2116                        + this.packetCallbacks.size()
2117                        + " left");
2118    }
2119
2120    public void sendDiscoTimeout() {
2121        if (mWaitForDisco.compareAndSet(true, false)) {
2122            Log.d(
2123                    Config.LOGTAG,
2124                    account.getJid().asBareJid() + ": finalizing bind after disco timeout");
2125            finalizeBind();
2126        }
2127    }
2128
2129    private void sendStartSession() {
2130        Log.d(
2131                Config.LOGTAG,
2132                account.getJid().asBareJid() + ": sending legacy session to outdated server");
2133        final Iq startSession = new Iq(Iq.Type.SET);
2134        startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
2135        this.sendUnmodifiedIqPacket(
2136                startSession,
2137                (packet) -> {
2138                    if (packet.getType() == Iq.Type.RESULT) {
2139                        final boolean waitForDisco = enableStreamManagement();
2140                        sendPostBindInitialization(waitForDisco, false);
2141                    } else if (packet.getType() != Iq.Type.TIMEOUT) {
2142                        throw new StateChangingError(Account.State.SESSION_FAILURE);
2143                    }
2144                },
2145                true);
2146    }
2147
2148    private boolean enableStreamManagement() {
2149        final boolean streamManagement = this.streamFeatures.streamManagement();
2150        if (streamManagement) {
2151            synchronized (this.mStanzaQueue) {
2152                final var enable = new Enable();
2153                tagWriter.writeStanzaAsync(enable);
2154                stanzasSent = 0;
2155                mStanzaQueue.clear();
2156            }
2157            return true;
2158        } else {
2159            return false;
2160        }
2161    }
2162
2163    private void sendPostBindInitialization(
2164            final boolean waitForDisco, final boolean carbonsEnabled) {
2165        features.carbonsEnabled = carbonsEnabled;
2166        features.blockListRequested = false;
2167        synchronized (this.disco) {
2168            this.disco.clear();
2169        }
2170        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
2171        mPendingServiceDiscoveries.set(0);
2172        mWaitForDisco.set(waitForDisco);
2173        this.lastDiscoStarted = SystemClock.elapsedRealtime();
2174        mXmppConnectionService.scheduleWakeUpCall(
2175                Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
2176        final Element caps = streamFeatures.findChild("c");
2177        final String hash = caps == null ? null : caps.getAttribute("hash");
2178        final String ver = caps == null ? null : caps.getAttribute("ver");
2179        ServiceDiscoveryResult discoveryResult = null;
2180        if (hash != null && ver != null) {
2181            discoveryResult =
2182                    mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
2183        }
2184        final boolean requestDiscoItemsFirst =
2185                !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
2186        if (requestDiscoItemsFirst) {
2187            sendServiceDiscoveryItems(account.getDomain());
2188        }
2189        if (discoveryResult == null) {
2190            sendServiceDiscoveryInfo(account.getDomain());
2191        } else {
2192            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
2193            disco.put(account.getDomain(), discoveryResult);
2194        }
2195        final var features = getFeatures();
2196        if (!features.bind2()) {
2197            discoverMamPreferences();
2198        }
2199        sendServiceDiscoveryInfo(account.getJid().asBareJid());
2200        if (!requestDiscoItemsFirst) {
2201            sendServiceDiscoveryItems(account.getDomain());
2202        }
2203
2204        if (!mWaitForDisco.get()) {
2205            finalizeBind();
2206        }
2207        this.lastSessionStarted = SystemClock.elapsedRealtime();
2208    }
2209
2210    private void sendServiceDiscoveryInfo(final Jid jid) {
2211        mPendingServiceDiscoveries.incrementAndGet();
2212        final Iq iq = new Iq(Iq.Type.GET);
2213        iq.setTo(jid);
2214        iq.query("http://jabber.org/protocol/disco#info");
2215        this.sendIqPacket(
2216                iq,
2217                (packet) -> {
2218                    if (packet.getType() == Iq.Type.RESULT) {
2219                        boolean advancedStreamFeaturesLoaded;
2220                        synchronized (XmppConnection.this.disco) {
2221                            ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
2222                            if (jid.equals(account.getDomain())) {
2223                                mXmppConnectionService.databaseBackend.insertDiscoveryResult(
2224                                        result);
2225                            }
2226                            disco.put(jid, result);
2227                            advancedStreamFeaturesLoaded =
2228                                    disco.containsKey(account.getDomain())
2229                                            && disco.containsKey(account.getJid().asBareJid());
2230                        }
2231                        if (advancedStreamFeaturesLoaded
2232                                && (jid.equals(account.getDomain())
2233                                        || jid.equals(account.getJid().asBareJid()))) {
2234                            enableAdvancedStreamFeatures();
2235                        }
2236                    } else if (packet.getType() == Iq.Type.ERROR) {
2237                        Log.d(
2238                                Config.LOGTAG,
2239                                account.getJid().asBareJid()
2240                                        + ": could not query disco info for "
2241                                        + jid.toString());
2242                        final boolean serverOrAccount =
2243                                jid.equals(account.getDomain())
2244                                        || jid.equals(account.getJid().asBareJid());
2245                        final boolean advancedStreamFeaturesLoaded;
2246                        if (serverOrAccount) {
2247                            synchronized (XmppConnection.this.disco) {
2248                                disco.put(jid, ServiceDiscoveryResult.empty());
2249                                advancedStreamFeaturesLoaded =
2250                                        disco.containsKey(account.getDomain())
2251                                                && disco.containsKey(account.getJid().asBareJid());
2252                            }
2253                        } else {
2254                            advancedStreamFeaturesLoaded = false;
2255                        }
2256                        if (advancedStreamFeaturesLoaded) {
2257                            enableAdvancedStreamFeatures();
2258                        }
2259                    }
2260                    if (packet.getType() != Iq.Type.TIMEOUT) {
2261                        if (mPendingServiceDiscoveries.decrementAndGet() == 0
2262                                && mWaitForDisco.compareAndSet(true, false)) {
2263                            finalizeBind();
2264                        }
2265                    }
2266                });
2267    }
2268
2269    private void discoverMamPreferences() {
2270        final Iq request = new Iq(Iq.Type.GET);
2271        request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2272        sendIqPacket(
2273                request,
2274                (response) -> {
2275                    if (response.getType() == Iq.Type.RESULT) {
2276                        Element prefs =
2277                                response.findChild(
2278                                        "prefs", MessageArchiveService.Version.MAM_2.namespace);
2279                        isMamPreferenceAlways =
2280                                "always"
2281                                        .equals(
2282                                                prefs == null
2283                                                        ? null
2284                                                        : prefs.getAttribute("default"));
2285                    }
2286                });
2287    }
2288
2289    private void discoverCommands() {
2290        final Iq request = new Iq(Iq.Type.GET);
2291        request.setTo(account.getDomain());
2292        request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2293        sendIqPacket(
2294                request,
2295                (response) -> {
2296                    if (response.getType() == Iq.Type.RESULT) {
2297                        final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2298                        if (query == null) {
2299                            return;
2300                        }
2301                        final HashMap<String, Jid> commands = new HashMap<>();
2302                        for (final Element child : query.getChildren()) {
2303                            if ("item".equals(child.getName())) {
2304                                final String node = child.getAttribute("node");
2305                                final Jid jid = child.getAttributeAsJid("jid");
2306                                if (node != null && jid != null) {
2307                                    commands.put(node, jid);
2308                                }
2309                            }
2310                        }
2311                        synchronized (this.commands) {
2312                            this.commands.clear();
2313                            this.commands.putAll(commands);
2314                        }
2315                    }
2316                });
2317    }
2318
2319    public boolean isMamPreferenceAlways() {
2320        return isMamPreferenceAlways;
2321    }
2322
2323    private void finalizeBind() {
2324        this.offlineMessagesRetrieved = false;
2325        this.bindListener.run();
2326        this.changeStatusToOnline();
2327    }
2328
2329    private void enableAdvancedStreamFeatures() {
2330        if (getFeatures().blocking() && !features.blockListRequested) {
2331            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2332            this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
2333        }
2334        for (final OnAdvancedStreamFeaturesLoaded listener :
2335                advancedStreamFeaturesLoadedListeners) {
2336            listener.onAdvancedStreamFeaturesAvailable(account);
2337        }
2338        if (getFeatures().carbons() && !features.carbonsEnabled) {
2339            sendEnableCarbons();
2340        }
2341        if (getFeatures().commands()) {
2342            discoverCommands();
2343        }
2344    }
2345
2346    private void sendServiceDiscoveryItems(final Jid server) {
2347        mPendingServiceDiscoveries.incrementAndGet();
2348        final Iq iq = new Iq(Iq.Type.GET);
2349        iq.setTo(server.getDomain());
2350        iq.query("http://jabber.org/protocol/disco#items");
2351        this.sendIqPacket(
2352                iq,
2353                (packet) -> {
2354                    if (packet.getType() == Iq.Type.RESULT) {
2355                        final HashSet<Jid> items = new HashSet<>();
2356                        final List<Element> elements = packet.query().getChildren();
2357                        for (final Element element : elements) {
2358                            if (element.getName().equals("item")) {
2359                                final Jid jid =
2360                                        Jid.Invalid.getNullForInvalid(
2361                                                element.getAttributeAsJid("jid"));
2362                                if (jid != null && !jid.equals(account.getDomain())) {
2363                                    items.add(jid);
2364                                }
2365                            }
2366                        }
2367                        for (Jid jid : items) {
2368                            sendServiceDiscoveryInfo(jid);
2369                        }
2370                    } else {
2371                        Log.d(
2372                                Config.LOGTAG,
2373                                account.getJid().asBareJid()
2374                                        + ": could not query disco items of "
2375                                        + server);
2376                    }
2377                    if (packet.getType() != Iq.Type.TIMEOUT) {
2378                        if (mPendingServiceDiscoveries.decrementAndGet() == 0
2379                                && mWaitForDisco.compareAndSet(true, false)) {
2380                            finalizeBind();
2381                        }
2382                    }
2383                });
2384    }
2385
2386    private void sendEnableCarbons() {
2387        final Iq iq = new Iq(Iq.Type.SET);
2388        iq.addChild("enable", Namespace.CARBONS);
2389        this.sendIqPacket(
2390                iq,
2391                (packet) -> {
2392                    if (packet.getType() == Iq.Type.RESULT) {
2393                        Log.d(
2394                                Config.LOGTAG,
2395                                account.getJid().asBareJid() + ": successfully enabled carbons");
2396                        features.carbonsEnabled = true;
2397                    } else {
2398                        Log.d(
2399                                Config.LOGTAG,
2400                                account.getJid().asBareJid()
2401                                        + ": could not enable carbons "
2402                                        + packet);
2403                    }
2404                });
2405    }
2406
2407    private void processStreamError(final StreamError streamError) throws IOException {
2408        final var loginInfo = this.loginInfo;
2409        final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo);
2410        if (isSecureLoggedIn && streamError.hasChild("conflict")) {
2411            if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
2412                this.appSettings.resetInstallationId();
2413            }
2414            account.setResource(createNewResource());
2415            Log.d(
2416                    Config.LOGTAG,
2417                    account.getJid().asBareJid()
2418                            + ": switching resource due to conflict ("
2419                            + account.getResource()
2420                            + ")");
2421            throw new IOException("Closed stream due to resource conflict");
2422        } else if (streamError.hasChild("host-unknown")) {
2423            throw new StateChangingException(Account.State.HOST_UNKNOWN);
2424        } else if (streamError.hasChild("policy-violation")) {
2425            this.lastConnectionStarted = SystemClock.elapsedRealtime();
2426            final String text = streamError.findChildContent("text");
2427            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2428            if (isSecureLoggedIn) {
2429                failPendingMessages(text);
2430            }
2431            throw new StateChangingException(Account.State.POLICY_VIOLATION);
2432        } else if (streamError.hasChild("see-other-host")) {
2433            final String seeOtherHost = streamError.findChildContent("see-other-host");
2434            final Resolver.Result currentResolverResult = this.currentResolverResult;
2435            if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2436                Log.d(
2437                        Config.LOGTAG,
2438                        account.getJid().asBareJid() + ": stream error " + streamError);
2439                throw new StateChangingException(Account.State.STREAM_ERROR);
2440            }
2441            Log.d(
2442                    Config.LOGTAG,
2443                    account.getJid().asBareJid()
2444                            + ": see other host: "
2445                            + seeOtherHost
2446                            + " "
2447                            + currentResolverResult);
2448            final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2449            if (seeOtherResult != null) {
2450                this.seeOtherHostResolverResult = seeOtherResult;
2451                throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2452            } else {
2453                throw new StateChangingException(Account.State.STREAM_ERROR);
2454            }
2455        } else {
2456            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2457            throw new StateChangingException(Account.State.STREAM_ERROR);
2458        }
2459    }
2460
2461    private void failPendingMessages(final String error) {
2462        synchronized (this.mStanzaQueue) {
2463            for (int i = 0; i < mStanzaQueue.size(); ++i) {
2464                final Stanza stanza = mStanzaQueue.valueAt(i);
2465                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) {
2466                    final String id = packet.getId();
2467                    final Jid to = packet.getTo();
2468                    mXmppConnectionService.markMessage(
2469                            account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2470                }
2471            }
2472        }
2473    }
2474
2475    private boolean establishStream(final SSLSockets.Version sslVersion)
2476            throws IOException, InterruptedException {
2477        final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2478        final SaslMechanism quickStartMechanism;
2479        if (secureConnection) {
2480            quickStartMechanism =
2481                    SaslMechanism.ensureAvailable(
2482                            account.getQuickStartMechanism(),
2483                            sslVersion,
2484                            appSettings.isRequireChannelBinding());
2485        } else {
2486            quickStartMechanism = null;
2487        }
2488        if (secureConnection
2489                && Config.QUICKSTART_ENABLED
2490                && quickStartMechanism != null
2491                && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2492            mXmppConnectionService.restoredFromDatabaseLatch.await();
2493            this.loginInfo =
2494                    new LoginInfo(
2495                            quickStartMechanism,
2496                            SaslMechanism.Version.SASL_2,
2497                            Bind2.QUICKSTART_FEATURES);
2498            final boolean usingFast = quickStartMechanism instanceof HashedToken;
2499            final AuthenticationRequest authenticate =
2500                    generateAuthenticationRequest(
2501                            quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
2502                            usingFast);
2503            authenticate.setMechanism(quickStartMechanism);
2504            sendStartStream(true, false);
2505            synchronized (this.mStanzaQueue) {
2506                this.stanzasSentBeforeAuthentication = this.stanzasSent;
2507                tagWriter.writeElement(authenticate);
2508            }
2509            Log.d(
2510                    Config.LOGTAG,
2511                    account.getJid().toString()
2512                            + ": quick start with "
2513                            + quickStartMechanism.getMechanism());
2514            return true;
2515        } else {
2516            sendStartStream(secureConnection, true);
2517            return false;
2518        }
2519    }
2520
2521    private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2522        final Tag stream = Tag.start("stream:stream");
2523        stream.setAttribute("to", account.getServer());
2524        if (from) {
2525            stream.setAttribute("from", account.getJid().asBareJid().toString());
2526        }
2527        stream.setAttribute("version", "1.0");
2528        stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2529        stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
2530        stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2531        tagWriter.writeTag(stream, flush);
2532    }
2533
2534    private static String createNewResource() {
2535        return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
2536    }
2537
2538    public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
2539        packet.setFrom(account.getJid());
2540        return this.sendUnmodifiedIqPacket(packet, callback, false);
2541    }
2542
2543    public synchronized String sendUnmodifiedIqPacket(
2544            final Iq packet, final Consumer<Iq> callback, boolean force) {
2545        // TODO if callback != null verify that type is get or set
2546        if (packet.getId() == null) {
2547            packet.setId(CryptoHelper.random(9));
2548        }
2549        if (callback != null) {
2550            synchronized (this.packetCallbacks) {
2551                packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2552            }
2553        }
2554        this.sendPacket(packet, force);
2555        return packet.getId();
2556    }
2557
2558    public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
2559        this.sendPacket(packet);
2560    }
2561
2562    public void sendPresencePacket(final Presence packet) {
2563        this.sendPacket(packet);
2564    }
2565
2566    private synchronized void sendPacket(final StreamElement packet) {
2567        sendPacket(packet, false);
2568    }
2569
2570    private synchronized void sendPacket(final StreamElement packet, final boolean force) {
2571        if (stanzasSent == Integer.MAX_VALUE) {
2572            resetStreamId();
2573            disconnect(true);
2574            return;
2575        }
2576        synchronized (this.mStanzaQueue) {
2577            if (force || isBound) {
2578                tagWriter.writeStanzaAsync(packet);
2579            } else {
2580                Log.d(
2581                        Config.LOGTAG,
2582                        account.getJid().asBareJid()
2583                                + " do not write stanza to unbound stream "
2584                                + packet.toString());
2585            }
2586            if (packet instanceof Stanza stanza) {
2587                if (this.mStanzaQueue.size() != 0) {
2588                    int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2589                    if (currentHighestKey != stanzasSent) {
2590                        throw new AssertionError("Stanza count messed up");
2591                    }
2592                }
2593
2594                ++stanzasSent;
2595                if (Config.EXTENDED_SM_LOGGING) {
2596                    Log.d(
2597                            Config.LOGTAG,
2598                            account.getJid().asBareJid()
2599                                    + ": counting outbound "
2600                                    + packet.getName()
2601                                    + " as #"
2602                                    + stanzasSent);
2603                }
2604                this.mStanzaQueue.append(stanzasSent, stanza);
2605                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message
2606                        && stanza.getId() != null
2607                        && inSmacksSession) {
2608                    if (Config.EXTENDED_SM_LOGGING) {
2609                        Log.d(
2610                                Config.LOGTAG,
2611                                account.getJid().asBareJid()
2612                                        + ": requesting ack for message stanza #"
2613                                        + stanzasSent);
2614                    }
2615                    tagWriter.writeStanzaAsync(new Request());
2616                }
2617            }
2618        }
2619    }
2620
2621    public void sendPing() {
2622        if (!r()) {
2623            final Iq iq = new Iq(Iq.Type.GET);
2624            iq.setFrom(account.getJid());
2625            iq.addChild("ping", Namespace.PING);
2626            this.sendIqPacket(iq, null);
2627        }
2628        this.lastPingSent = SystemClock.elapsedRealtime();
2629    }
2630
2631    public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2632        this.jingleListener = listener;
2633    }
2634
2635    public void setOnStatusChangedListener(final OnStatusChanged listener) {
2636        this.statusListener = listener;
2637    }
2638
2639    public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2640        this.acknowledgedListener = listener;
2641    }
2642
2643    public void addOnAdvancedStreamFeaturesAvailableListener(
2644            final OnAdvancedStreamFeaturesLoaded listener) {
2645        this.advancedStreamFeaturesLoadedListeners.add(listener);
2646    }
2647
2648    private void forceCloseSocket() {
2649        FileBackend.close(this.socket);
2650        FileBackend.close(this.tagReader);
2651    }
2652
2653    public void interrupt() {
2654        if (this.mThread != null) {
2655            this.mThread.interrupt();
2656        }
2657    }
2658
2659    public void disconnect(final boolean force) {
2660        interrupt();
2661        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2662        if (force) {
2663            forceCloseSocket();
2664        } else {
2665            final TagWriter currentTagWriter = this.tagWriter;
2666            if (currentTagWriter.isActive()) {
2667                currentTagWriter.finish();
2668                final Socket currentSocket = this.socket;
2669                final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2670                try {
2671                    currentTagWriter.await(1, TimeUnit.SECONDS);
2672                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2673                    currentTagWriter.writeTag(Tag.end("stream:stream"));
2674                    if (streamCountDownLatch != null) {
2675                        if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2676                            Log.d(
2677                                    Config.LOGTAG,
2678                                    account.getJid().asBareJid() + ": remote ended stream");
2679                        } else {
2680                            Log.d(
2681                                    Config.LOGTAG,
2682                                    account.getJid().asBareJid()
2683                                            + ": remote has not closed socket. force closing");
2684                        }
2685                    }
2686                } catch (InterruptedException e) {
2687                    Log.d(
2688                            Config.LOGTAG,
2689                            account.getJid().asBareJid()
2690                                    + ": interrupted while gracefully closing stream");
2691                } catch (final IOException e) {
2692                    Log.d(
2693                            Config.LOGTAG,
2694                            account.getJid().asBareJid()
2695                                    + ": io exception during disconnect ("
2696                                    + e.getMessage()
2697                                    + ")");
2698                } finally {
2699                    FileBackend.close(currentSocket);
2700                }
2701            } else {
2702                forceCloseSocket();
2703            }
2704        }
2705    }
2706
2707    private void resetStreamId() {
2708        this.pendingResumeId.clear();
2709        this.streamId = null;
2710        this.boundStreamFeatures = null;
2711    }
2712
2713    private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2714        synchronized (this.disco) {
2715            final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2716            for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2717                if (cursor.getValue().getFeatures().contains(feature)) {
2718                    items.add(cursor);
2719                }
2720            }
2721            return items;
2722        }
2723    }
2724
2725    public Jid findDiscoItemByFeature(final String feature) {
2726        final var items = findDiscoItemsByFeature(feature);
2727        if (items.isEmpty()) {
2728            return null;
2729        }
2730        return Iterables.getFirst(items, null).getKey();
2731    }
2732
2733    public boolean r() {
2734        if (getFeatures().sm()) {
2735            this.tagWriter.writeStanzaAsync(new Request());
2736            return true;
2737        } else {
2738            return false;
2739        }
2740    }
2741
2742    public List<String> getMucServersWithholdAccount() {
2743        final List<String> servers = getMucServers();
2744        servers.remove(account.getDomain().toString());
2745        return servers;
2746    }
2747
2748    public List<String> getMucServers() {
2749        List<String> servers = new ArrayList<>();
2750        synchronized (this.disco) {
2751            for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2752                final ServiceDiscoveryResult value = cursor.getValue();
2753                if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2754                        && value.hasIdentity("conference", "text")
2755                        && !value.getFeatures().contains("jabber:iq:gateway")
2756                        && !value.hasIdentity("conference", "irc")) {
2757                    servers.add(cursor.getKey().toString());
2758                }
2759            }
2760        }
2761        return servers;
2762    }
2763
2764    public String getMucServer() {
2765        return Iterables.getFirst(getMucServers(), null);
2766    }
2767
2768    public int getTimeToNextAttempt(final boolean aggressive) {
2769        final int interval;
2770        if (aggressive) {
2771            interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2772        } else {
2773            final int additionalTime =
2774                    account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2775            interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2776        }
2777        final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000);
2778        return interval - connectionDuration;
2779    }
2780
2781    public int getAttempt() {
2782        return this.attempt;
2783    }
2784
2785    public Features getFeatures() {
2786        return this.features;
2787    }
2788
2789    public long getLastSessionEstablished() {
2790        final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2791        return System.currentTimeMillis() - diff;
2792    }
2793
2794    public long getConnectionDuration() {
2795        return SystemClock.elapsedRealtime() - this.lastConnectionStarted;
2796    }
2797
2798    public long getDiscoDuration() {
2799        return SystemClock.elapsedRealtime() - this.lastDiscoStarted;
2800    }
2801
2802    public long getLastPingSent() {
2803        return this.lastPingSent;
2804    }
2805
2806    public long getLastPacketReceived() {
2807        return this.lastPacketReceived;
2808    }
2809
2810    public void sendActive() {
2811        this.sendPacket(new Active());
2812    }
2813
2814    public void sendInactive() {
2815        this.sendPacket(new Inactive());
2816    }
2817
2818    public void resetAttemptCount(boolean resetConnectTime) {
2819        this.attempt = 0;
2820        if (resetConnectTime) {
2821            this.lastConnectionStarted = 0;
2822        }
2823    }
2824
2825    public void setInteractive(boolean interactive) {
2826        this.mInteractive = interactive;
2827    }
2828
2829    private IqGenerator getIqGenerator() {
2830        return mXmppConnectionService.getIqGenerator();
2831    }
2832
2833    public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
2834        if (trackOfflineMessageRetrieval) {
2835            final Iq iqPing = new Iq(Iq.Type.GET);
2836            iqPing.addChild("ping", Namespace.PING);
2837            this.sendIqPacket(
2838                    iqPing,
2839                    (response) -> {
2840                        Log.d(
2841                                Config.LOGTAG,
2842                                account.getJid().asBareJid()
2843                                        + ": got ping response after sending initial presence");
2844                        XmppConnection.this.offlineMessagesRetrieved = true;
2845                    });
2846        } else {
2847            this.offlineMessagesRetrieved = true;
2848        }
2849    }
2850
2851    public boolean isOfflineMessagesRetrieved() {
2852        return this.offlineMessagesRetrieved;
2853    }
2854
2855    public void fetchRoster() {
2856        final Iq iqPacket = new Iq(Iq.Type.GET);
2857        final var version = account.getRosterVersion();
2858        if (Strings.isNullOrEmpty(account.getRosterVersion())) {
2859            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
2860        } else {
2861            Log.d(
2862                    Config.LOGTAG,
2863                    account.getJid().asBareJid() + ": fetching roster version " + version);
2864        }
2865        iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
2866        sendIqPacket(iqPacket, unregisteredIqListener);
2867    }
2868
2869    public void triggerConnectionTimeout() {
2870        final var duration = getConnectionDuration();
2871        Log.d(
2872                Config.LOGTAG,
2873                account.getJid().asBareJid() + ": connection timeout after " + duration + "ms");
2874
2875        // last connection time gets reset so time to next attempt is calculated correctly
2876        this.lastConnectionStarted = SystemClock.elapsedRealtime();
2877
2878        // interrupt needs to be called before status change; otherwise we interrupt the newly
2879        // created thread
2880        this.interrupt();
2881        this.forceCloseSocket();
2882        this.changeStatus(Account.State.CONNECTION_TIMEOUT);
2883    }
2884
2885    private class MyKeyManager implements X509KeyManager {
2886        @Override
2887        public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2888            return account.getPrivateKeyAlias();
2889        }
2890
2891        @Override
2892        public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2893            return null;
2894        }
2895
2896        @Override
2897        public X509Certificate[] getCertificateChain(String alias) {
2898            Log.d(Config.LOGTAG, "getting certificate chain");
2899            try {
2900                return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2901            } catch (final Exception e) {
2902                Log.d(Config.LOGTAG, "could not get certificate chain", e);
2903                return new X509Certificate[0];
2904            }
2905        }
2906
2907        @Override
2908        public String[] getClientAliases(String s, Principal[] principals) {
2909            final String alias = account.getPrivateKeyAlias();
2910            return alias != null ? new String[] {alias} : new String[0];
2911        }
2912
2913        @Override
2914        public String[] getServerAliases(String s, Principal[] principals) {
2915            return new String[0];
2916        }
2917
2918        @Override
2919        public PrivateKey getPrivateKey(String alias) {
2920            try {
2921                return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2922            } catch (Exception e) {
2923                return null;
2924            }
2925        }
2926    }
2927
2928    private static class LoginInfo {
2929        public final SaslMechanism saslMechanism;
2930        public final SaslMechanism.Version saslVersion;
2931        public final List<String> inlineBindFeatures;
2932        public final AtomicBoolean success = new AtomicBoolean(false);
2933
2934        private LoginInfo(
2935                final SaslMechanism saslMechanism,
2936                final SaslMechanism.Version saslVersion,
2937                final Collection<String> inlineBindFeatures) {
2938            Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
2939            Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
2940            this.saslMechanism = saslMechanism;
2941            this.saslVersion = saslVersion;
2942            this.inlineBindFeatures =
2943                    inlineBindFeatures == null
2944                            ? Collections.emptyList()
2945                            : ImmutableList.copyOf(inlineBindFeatures);
2946        }
2947
2948        public static SaslMechanism mechanism(final LoginInfo loginInfo) {
2949            return loginInfo == null ? null : loginInfo.saslMechanism;
2950        }
2951
2952        public void success(final String challenge, final SSLSocket sslSocket)
2953                throws SaslMechanism.AuthenticationException {
2954            if (Thread.currentThread().isInterrupted()) {
2955                throw new SaslMechanism.AuthenticationException("Race condition during auth");
2956            }
2957            final var response = this.saslMechanism.getResponse(challenge, sslSocket);
2958            if (!Strings.isNullOrEmpty(response)) {
2959                throw new SaslMechanism.AuthenticationException(
2960                        "processing success yielded another response");
2961            }
2962            if (this.success.compareAndSet(false, true)) {
2963                return;
2964            }
2965            throw new SaslMechanism.AuthenticationException("Process 'success' twice");
2966        }
2967
2968        public static boolean isSuccess(final LoginInfo loginInfo) {
2969            return loginInfo != null && loginInfo.success.get();
2970        }
2971    }
2972
2973    private static class StreamId {
2974        public final String id;
2975        public final Resolver.Result location;
2976
2977        private StreamId(String id, Resolver.Result location) {
2978            this.id = id;
2979            this.location = location;
2980        }
2981
2982        @NonNull
2983        @Override
2984        public String toString() {
2985            return MoreObjects.toStringHelper(this)
2986                    .add("id", id)
2987                    .add("location", location)
2988                    .toString();
2989        }
2990    }
2991
2992    private static class StateChangingError extends Error {
2993        private final Account.State state;
2994
2995        public StateChangingError(Account.State state) {
2996            this.state = state;
2997        }
2998    }
2999
3000    private static class StateChangingException extends IOException {
3001        private final Account.State state;
3002
3003        public StateChangingException(Account.State state) {
3004            this.state = state;
3005        }
3006    }
3007
3008    public class Features {
3009        XmppConnection connection;
3010        private boolean carbonsEnabled = false;
3011        private boolean encryptionEnabled = false;
3012        private boolean blockListRequested = false;
3013
3014        public Features(final XmppConnection connection) {
3015            this.connection = connection;
3016        }
3017
3018        private boolean hasDiscoFeature(final Jid server, final String feature) {
3019            synchronized (XmppConnection.this.disco) {
3020                final ServiceDiscoveryResult sdr = connection.disco.get(server);
3021                return sdr != null && sdr.getFeatures().contains(feature);
3022            }
3023        }
3024
3025        public boolean carbons() {
3026            return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
3027        }
3028
3029        public boolean commands() {
3030            return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
3031        }
3032
3033        public boolean easyOnboardingInvites() {
3034            synchronized (commands) {
3035                return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
3036            }
3037        }
3038
3039        public boolean bookmarksConversion() {
3040            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
3041                    && pepPublishOptions();
3042        }
3043
3044        public boolean blocking() {
3045            return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
3046        }
3047
3048        public boolean spamReporting() {
3049            return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
3050        }
3051
3052        public boolean flexibleOfflineMessageRetrieval() {
3053            return hasDiscoFeature(
3054                    account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
3055        }
3056
3057        public boolean register() {
3058            return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
3059        }
3060
3061        public boolean invite() {
3062            return connection.streamFeatures != null
3063                    && connection.streamFeatures.hasChild("register", Namespace.INVITE);
3064        }
3065
3066        public boolean sm() {
3067            return streamId != null
3068                    || (connection.streamFeatures != null
3069                            && connection.streamFeatures.streamManagement());
3070        }
3071
3072        public boolean csi() {
3073            return connection.streamFeatures != null
3074                    && connection.streamFeatures.clientStateIndication();
3075        }
3076
3077        public boolean pep() {
3078            synchronized (XmppConnection.this.disco) {
3079                ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
3080                return info != null && info.hasIdentity("pubsub", "pep");
3081            }
3082        }
3083
3084        public boolean pepPersistent() {
3085            synchronized (XmppConnection.this.disco) {
3086                ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
3087                return info != null
3088                        && info.getFeatures()
3089                                .contains("http://jabber.org/protocol/pubsub#persistent-items");
3090            }
3091        }
3092
3093        public boolean bind2() {
3094            final var loginInfo = XmppConnection.this.loginInfo;
3095            return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty();
3096        }
3097
3098        public boolean sasl2() {
3099            final var loginInfo = XmppConnection.this.loginInfo;
3100            return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2;
3101        }
3102
3103        public String loginMechanism() {
3104            final var loginInfo = XmppConnection.this.loginInfo;
3105            return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism();
3106        }
3107
3108        public boolean pepPublishOptions() {
3109            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
3110        }
3111
3112        public boolean pepConfigNodeMax() {
3113            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX);
3114        }
3115
3116        public boolean pepOmemoWhitelisted() {
3117            return hasDiscoFeature(
3118                    account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
3119        }
3120
3121        public boolean mam() {
3122            return MessageArchiveService.Version.has(getAccountFeatures());
3123        }
3124
3125        public List<String> getAccountFeatures() {
3126            ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
3127            return result == null ? Collections.emptyList() : result.getFeatures();
3128        }
3129
3130        public boolean push() {
3131            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
3132                    || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
3133        }
3134
3135        public boolean rosterVersioning() {
3136            return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
3137        }
3138
3139        public void setBlockListRequested(boolean value) {
3140            this.blockListRequested = value;
3141        }
3142
3143        public boolean httpUpload(long filesize) {
3144            if (Config.DISABLE_HTTP_UPLOAD) {
3145                return false;
3146            } else {
3147                for (String namespace :
3148                        new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
3149                    List<Entry<Jid, ServiceDiscoveryResult>> items =
3150                            findDiscoItemsByFeature(namespace);
3151                    if (!items.isEmpty()) {
3152                        try {
3153                            long maxsize =
3154                                    Long.parseLong(
3155                                            items.get(0)
3156                                                    .getValue()
3157                                                    .getExtendedDiscoInformation(
3158                                                            namespace, "max-file-size"));
3159                            if (filesize <= maxsize) {
3160                                return true;
3161                            } else {
3162                                Log.d(
3163                                        Config.LOGTAG,
3164                                        account.getJid().asBareJid()
3165                                                + ": http upload is not available for files with"
3166                                                + " size "
3167                                                + filesize
3168                                                + " (max is "
3169                                                + maxsize
3170                                                + ")");
3171                                return false;
3172                            }
3173                        } catch (Exception e) {
3174                            return true;
3175                        }
3176                    }
3177                }
3178                return false;
3179            }
3180        }
3181
3182        public boolean useLegacyHttpUpload() {
3183            return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
3184                    && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
3185        }
3186
3187        public long getMaxHttpUploadSize() {
3188            for (String namespace :
3189                    new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
3190                List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
3191                if (!items.isEmpty()) {
3192                    try {
3193                        return Long.parseLong(
3194                                items.get(0)
3195                                        .getValue()
3196                                        .getExtendedDiscoInformation(namespace, "max-file-size"));
3197                    } catch (Exception e) {
3198                        // ignored
3199                    }
3200                }
3201            }
3202            return -1;
3203        }
3204
3205        public boolean stanzaIds() {
3206            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3207        }
3208
3209        public boolean bookmarks2() {
3210            return pepPublishOptions()
3211                    && pepConfigNodeMax()
3212                    && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3213        }
3214
3215        public boolean externalServiceDiscovery() {
3216            return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3217        }
3218
3219        public boolean mds() {
3220            return pepPublishOptions()
3221                    && pepConfigNodeMax()
3222                    && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION;
3223        }
3224
3225        public boolean mdsServerAssist() {
3226            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
3227        }
3228    }
3229}