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