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