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