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