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