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