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