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