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