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