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