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