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