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