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 final var features = getFeatures();
2107 if (!features.bind2()) {
2108 discoverMamPreferences();
2109 }
2110 sendServiceDiscoveryInfo(account.getJid().asBareJid());
2111 if (!requestDiscoItemsFirst) {
2112 sendServiceDiscoveryItems(account.getDomain());
2113 }
2114
2115 if (!mWaitForDisco.get()) {
2116 finalizeBind();
2117 }
2118 this.lastSessionStarted = SystemClock.elapsedRealtime();
2119 }
2120
2121 private void sendServiceDiscoveryInfo(final Jid jid) {
2122 mPendingServiceDiscoveries.incrementAndGet();
2123 final Iq iq = new Iq(Iq.Type.GET);
2124 iq.setTo(jid);
2125 iq.query("http://jabber.org/protocol/disco#info");
2126 this.sendIqPacket(
2127 iq,
2128 (packet) -> {
2129 if (packet.getType() == Iq.Type.RESULT) {
2130 boolean advancedStreamFeaturesLoaded;
2131 synchronized (XmppConnection.this.disco) {
2132 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
2133 if (jid.equals(account.getDomain())) {
2134 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
2135 result);
2136 }
2137 disco.put(jid, result);
2138 advancedStreamFeaturesLoaded =
2139 disco.containsKey(account.getDomain())
2140 && disco.containsKey(account.getJid().asBareJid());
2141 }
2142 if (advancedStreamFeaturesLoaded
2143 && (jid.equals(account.getDomain())
2144 || jid.equals(account.getJid().asBareJid()))) {
2145 enableAdvancedStreamFeatures();
2146 }
2147 } else if (packet.getType() == Iq.Type.ERROR) {
2148 Log.d(
2149 Config.LOGTAG,
2150 account.getJid().asBareJid()
2151 + ": could not query disco info for "
2152 + jid.toString());
2153 final boolean serverOrAccount =
2154 jid.equals(account.getDomain())
2155 || jid.equals(account.getJid().asBareJid());
2156 final boolean advancedStreamFeaturesLoaded;
2157 if (serverOrAccount) {
2158 synchronized (XmppConnection.this.disco) {
2159 disco.put(jid, ServiceDiscoveryResult.empty());
2160 advancedStreamFeaturesLoaded =
2161 disco.containsKey(account.getDomain())
2162 && disco.containsKey(account.getJid().asBareJid());
2163 }
2164 } else {
2165 advancedStreamFeaturesLoaded = false;
2166 }
2167 if (advancedStreamFeaturesLoaded) {
2168 enableAdvancedStreamFeatures();
2169 }
2170 }
2171 if (packet.getType() != Iq.Type.TIMEOUT) {
2172 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2173 && mWaitForDisco.compareAndSet(true, false)) {
2174 finalizeBind();
2175 }
2176 }
2177 });
2178 }
2179
2180 private void discoverMamPreferences() {
2181 final Iq request = new Iq(Iq.Type.GET);
2182 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2183 sendIqPacket(
2184 request,
2185 (response) -> {
2186 if (response.getType() == Iq.Type.RESULT) {
2187 Element prefs =
2188 response.findChild(
2189 "prefs", MessageArchiveService.Version.MAM_2.namespace);
2190 isMamPreferenceAlways =
2191 "always"
2192 .equals(
2193 prefs == null
2194 ? null
2195 : prefs.getAttribute("default"));
2196 }
2197 });
2198 }
2199
2200 private void discoverCommands() {
2201 final Iq request = new Iq(Iq.Type.GET);
2202 request.setTo(account.getDomain());
2203 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2204 sendIqPacket(
2205 request,
2206 (response) -> {
2207 if (response.getType() == Iq.Type.RESULT) {
2208 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2209 if (query == null) {
2210 return;
2211 }
2212 final HashMap<String, Jid> commands = new HashMap<>();
2213 for (final Element child : query.getChildren()) {
2214 if ("item".equals(child.getName())) {
2215 final String node = child.getAttribute("node");
2216 final Jid jid = child.getAttributeAsJid("jid");
2217 if (node != null && jid != null) {
2218 commands.put(node, jid);
2219 }
2220 }
2221 }
2222 synchronized (this.commands) {
2223 this.commands.clear();
2224 this.commands.putAll(commands);
2225 }
2226 }
2227 });
2228 }
2229
2230 public boolean isMamPreferenceAlways() {
2231 return isMamPreferenceAlways;
2232 }
2233
2234 private void finalizeBind() {
2235 this.offlineMessagesRetrieved = false;
2236 this.bindListener.run();
2237 this.changeStatusToOnline();
2238 }
2239
2240 private void enableAdvancedStreamFeatures() {
2241 if (getFeatures().blocking() && !features.blockListRequested) {
2242 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2243 this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
2244 }
2245 for (final OnAdvancedStreamFeaturesLoaded listener :
2246 advancedStreamFeaturesLoadedListeners) {
2247 listener.onAdvancedStreamFeaturesAvailable(account);
2248 }
2249 if (getFeatures().carbons() && !features.carbonsEnabled) {
2250 sendEnableCarbons();
2251 }
2252 if (getFeatures().commands()) {
2253 discoverCommands();
2254 }
2255 }
2256
2257 private void sendServiceDiscoveryItems(final Jid server) {
2258 mPendingServiceDiscoveries.incrementAndGet();
2259 final Iq iq = new Iq(Iq.Type.GET);
2260 iq.setTo(server.getDomain());
2261 iq.query("http://jabber.org/protocol/disco#items");
2262 this.sendIqPacket(
2263 iq,
2264 (packet) -> {
2265 if (packet.getType() == Iq.Type.RESULT) {
2266 final HashSet<Jid> items = new HashSet<>();
2267 final List<Element> elements = packet.query().getChildren();
2268 for (final Element element : elements) {
2269 if (element.getName().equals("item")) {
2270 final Jid jid =
2271 InvalidJid.getNullForInvalid(
2272 element.getAttributeAsJid("jid"));
2273 if (jid != null && !jid.equals(account.getDomain())) {
2274 items.add(jid);
2275 }
2276 }
2277 }
2278 for (Jid jid : items) {
2279 sendServiceDiscoveryInfo(jid);
2280 }
2281 } else {
2282 Log.d(
2283 Config.LOGTAG,
2284 account.getJid().asBareJid()
2285 + ": could not query disco items of "
2286 + server);
2287 }
2288 if (packet.getType() != Iq.Type.TIMEOUT) {
2289 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2290 && mWaitForDisco.compareAndSet(true, false)) {
2291 finalizeBind();
2292 }
2293 }
2294 });
2295 }
2296
2297 private void sendEnableCarbons() {
2298 final Iq iq = new Iq(Iq.Type.SET);
2299 iq.addChild("enable", Namespace.CARBONS);
2300 this.sendIqPacket(
2301 iq,
2302 (packet) -> {
2303 if (packet.getType() == Iq.Type.RESULT) {
2304 Log.d(
2305 Config.LOGTAG,
2306 account.getJid().asBareJid() + ": successfully enabled carbons");
2307 features.carbonsEnabled = true;
2308 } else {
2309 Log.d(
2310 Config.LOGTAG,
2311 account.getJid().asBareJid()
2312 + ": could not enable carbons "
2313 + packet);
2314 }
2315 });
2316 }
2317
2318 private void processStreamError(final Tag currentTag) throws IOException {
2319 final Element streamError = tagReader.readElement(currentTag);
2320 if (streamError == null) {
2321 return;
2322 }
2323 if (streamError.hasChild("conflict")) {
2324 final var loginInfo = this.loginInfo;
2325 if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) {
2326 this.appSettings.resetInstallationId();
2327 }
2328 account.setResource(createNewResource());
2329 Log.d(
2330 Config.LOGTAG,
2331 account.getJid().asBareJid()
2332 + ": switching resource due to conflict ("
2333 + account.getResource()
2334 + ")");
2335 throw new IOException("Closed stream due to resource conflict");
2336 } else if (streamError.hasChild("host-unknown")) {
2337 throw new StateChangingException(Account.State.HOST_UNKNOWN);
2338 } else if (streamError.hasChild("policy-violation")) {
2339 this.lastConnect = SystemClock.elapsedRealtime();
2340 final String text = streamError.findChildContent("text");
2341 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2342 failPendingMessages(text);
2343 throw new StateChangingException(Account.State.POLICY_VIOLATION);
2344 } else if (streamError.hasChild("see-other-host")) {
2345 final String seeOtherHost = streamError.findChildContent("see-other-host");
2346 final Resolver.Result currentResolverResult = this.currentResolverResult;
2347 if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2348 Log.d(
2349 Config.LOGTAG,
2350 account.getJid().asBareJid() + ": stream error " + streamError);
2351 throw new StateChangingException(Account.State.STREAM_ERROR);
2352 }
2353 Log.d(
2354 Config.LOGTAG,
2355 account.getJid().asBareJid()
2356 + ": see other host: "
2357 + seeOtherHost
2358 + " "
2359 + currentResolverResult);
2360 final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2361 if (seeOtherResult != null) {
2362 this.seeOtherHostResolverResult = seeOtherResult;
2363 throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2364 } else {
2365 throw new StateChangingException(Account.State.STREAM_ERROR);
2366 }
2367 } else {
2368 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2369 throw new StateChangingException(Account.State.STREAM_ERROR);
2370 }
2371 }
2372
2373 private void failPendingMessages(final String error) {
2374 synchronized (this.mStanzaQueue) {
2375 for (int i = 0; i < mStanzaQueue.size(); ++i) {
2376 final Stanza stanza = mStanzaQueue.valueAt(i);
2377 if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) {
2378 final String id = packet.getId();
2379 final Jid to = packet.getTo();
2380 mXmppConnectionService.markMessage(
2381 account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
2382 }
2383 }
2384 }
2385 }
2386
2387 private boolean establishStream(final SSLSockets.Version sslVersion)
2388 throws IOException, InterruptedException {
2389 final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
2390 final SaslMechanism quickStartMechanism;
2391 if (secureConnection) {
2392 quickStartMechanism =
2393 SaslMechanism.ensureAvailable(
2394 account.getQuickStartMechanism(),
2395 sslVersion,
2396 appSettings.isRequireChannelBinding());
2397 } else {
2398 quickStartMechanism = null;
2399 }
2400 if (secureConnection
2401 && Config.QUICKSTART_ENABLED
2402 && quickStartMechanism != null
2403 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
2404 mXmppConnectionService.restoredFromDatabaseLatch.await();
2405 this.loginInfo =
2406 new LoginInfo(
2407 quickStartMechanism,
2408 SaslMechanism.Version.SASL_2,
2409 Bind2.QUICKSTART_FEATURES);
2410 final boolean usingFast = quickStartMechanism instanceof HashedToken;
2411 final AuthenticationRequest authenticate =
2412 generateAuthenticationRequest(
2413 quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
2414 usingFast);
2415 authenticate.setMechanism(quickStartMechanism);
2416 sendStartStream(true, false);
2417 synchronized (this.mStanzaQueue) {
2418 this.stanzasSentBeforeAuthentication = this.stanzasSent;
2419 tagWriter.writeElement(authenticate);
2420 }
2421 Log.d(
2422 Config.LOGTAG,
2423 account.getJid().toString()
2424 + ": quick start with "
2425 + quickStartMechanism.getMechanism());
2426 return true;
2427 } else {
2428 sendStartStream(secureConnection, true);
2429 return false;
2430 }
2431 }
2432
2433 private void sendStartStream(final boolean from, final boolean flush) throws IOException {
2434 final Tag stream = Tag.start("stream:stream");
2435 stream.setAttribute("to", account.getServer());
2436 if (from) {
2437 stream.setAttribute("from", account.getJid().asBareJid().toEscapedString());
2438 }
2439 stream.setAttribute("version", "1.0");
2440 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
2441 stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
2442 stream.setAttribute("xmlns:stream", Namespace.STREAMS);
2443 tagWriter.writeTag(stream, flush);
2444 }
2445
2446 private static String createNewResource() {
2447 return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
2448 }
2449
2450 public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
2451 packet.setFrom(account.getJid());
2452 return this.sendUnmodifiedIqPacket(packet, callback, false);
2453 }
2454
2455 public synchronized String sendUnmodifiedIqPacket(
2456 final Iq packet, final Consumer<Iq> callback, boolean force) {
2457 // TODO if callback != null verify that type is get or set
2458 if (packet.getId() == null) {
2459 packet.setId(CryptoHelper.random(9));
2460 }
2461 if (callback != null) {
2462 synchronized (this.packetCallbacks) {
2463 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
2464 }
2465 }
2466 this.sendPacket(packet, force);
2467 return packet.getId();
2468 }
2469
2470 public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
2471 this.sendPacket(packet);
2472 }
2473
2474 public void sendPresencePacket(final Presence packet) {
2475 this.sendPacket(packet);
2476 }
2477
2478 private synchronized void sendPacket(final StreamElement packet) {
2479 sendPacket(packet, false);
2480 }
2481
2482 private synchronized void sendPacket(final StreamElement packet, final boolean force) {
2483 if (stanzasSent == Integer.MAX_VALUE) {
2484 resetStreamId();
2485 disconnect(true);
2486 return;
2487 }
2488 synchronized (this.mStanzaQueue) {
2489 if (force || isBound) {
2490 tagWriter.writeStanzaAsync(packet);
2491 } else {
2492 Log.d(
2493 Config.LOGTAG,
2494 account.getJid().asBareJid()
2495 + " do not write stanza to unbound stream "
2496 + packet.toString());
2497 }
2498 if (packet instanceof Stanza stanza) {
2499 if (this.mStanzaQueue.size() != 0) {
2500 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2501 if (currentHighestKey != stanzasSent) {
2502 throw new AssertionError("Stanza count messed up");
2503 }
2504 }
2505
2506 ++stanzasSent;
2507 if (Config.EXTENDED_SM_LOGGING) {
2508 Log.d(
2509 Config.LOGTAG,
2510 account.getJid().asBareJid()
2511 + ": counting outbound "
2512 + packet.getName()
2513 + " as #"
2514 + stanzasSent);
2515 }
2516 this.mStanzaQueue.append(stanzasSent, stanza);
2517 if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message
2518 && stanza.getId() != null
2519 && inSmacksSession) {
2520 if (Config.EXTENDED_SM_LOGGING) {
2521 Log.d(
2522 Config.LOGTAG,
2523 account.getJid().asBareJid()
2524 + ": requesting ack for message stanza #"
2525 + stanzasSent);
2526 }
2527 tagWriter.writeStanzaAsync(new Request());
2528 }
2529 }
2530 }
2531 }
2532
2533 public void sendPing() {
2534 if (!r()) {
2535 final Iq iq = new Iq(Iq.Type.GET);
2536 iq.setFrom(account.getJid());
2537 iq.addChild("ping", Namespace.PING);
2538 this.sendIqPacket(iq, null);
2539 }
2540 this.lastPingSent = SystemClock.elapsedRealtime();
2541 }
2542
2543 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2544 this.jingleListener = listener;
2545 }
2546
2547 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2548 this.statusListener = listener;
2549 }
2550
2551 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2552 this.acknowledgedListener = listener;
2553 }
2554
2555 public void addOnAdvancedStreamFeaturesAvailableListener(
2556 final OnAdvancedStreamFeaturesLoaded listener) {
2557 this.advancedStreamFeaturesLoadedListeners.add(listener);
2558 }
2559
2560 private void forceCloseSocket() {
2561 FileBackend.close(this.socket);
2562 FileBackend.close(this.tagReader);
2563 }
2564
2565 public void interrupt() {
2566 if (this.mThread != null) {
2567 this.mThread.interrupt();
2568 }
2569 }
2570
2571 public void disconnect(final boolean force) {
2572 interrupt();
2573 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2574 if (force) {
2575 forceCloseSocket();
2576 } else {
2577 final TagWriter currentTagWriter = this.tagWriter;
2578 if (currentTagWriter.isActive()) {
2579 currentTagWriter.finish();
2580 final Socket currentSocket = this.socket;
2581 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2582 try {
2583 currentTagWriter.await(1, TimeUnit.SECONDS);
2584 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2585 currentTagWriter.writeTag(Tag.end("stream:stream"));
2586 if (streamCountDownLatch != null) {
2587 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2588 Log.d(
2589 Config.LOGTAG,
2590 account.getJid().asBareJid() + ": remote ended stream");
2591 } else {
2592 Log.d(
2593 Config.LOGTAG,
2594 account.getJid().asBareJid()
2595 + ": remote has not closed socket. force closing");
2596 }
2597 }
2598 } catch (InterruptedException e) {
2599 Log.d(
2600 Config.LOGTAG,
2601 account.getJid().asBareJid()
2602 + ": interrupted while gracefully closing stream");
2603 } catch (final IOException e) {
2604 Log.d(
2605 Config.LOGTAG,
2606 account.getJid().asBareJid()
2607 + ": io exception during disconnect ("
2608 + e.getMessage()
2609 + ")");
2610 } finally {
2611 FileBackend.close(currentSocket);
2612 }
2613 } else {
2614 forceCloseSocket();
2615 }
2616 }
2617 }
2618
2619 private void resetStreamId() {
2620 this.streamId = null;
2621 this.boundStreamFeatures = null;
2622 }
2623
2624 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2625 synchronized (this.disco) {
2626 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2627 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2628 if (cursor.getValue().getFeatures().contains(feature)) {
2629 items.add(cursor);
2630 }
2631 }
2632 return items;
2633 }
2634 }
2635
2636 public Jid findDiscoItemByFeature(final String feature) {
2637 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2638 if (items.size() >= 1) {
2639 return items.get(0).getKey();
2640 }
2641 return null;
2642 }
2643
2644 public boolean r() {
2645 if (getFeatures().sm()) {
2646 this.tagWriter.writeStanzaAsync(new Request());
2647 return true;
2648 } else {
2649 return false;
2650 }
2651 }
2652
2653 public List<String> getMucServersWithholdAccount() {
2654 final List<String> servers = getMucServers();
2655 servers.remove(account.getDomain().toEscapedString());
2656 return servers;
2657 }
2658
2659 public List<String> getMucServers() {
2660 List<String> servers = new ArrayList<>();
2661 synchronized (this.disco) {
2662 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2663 final ServiceDiscoveryResult value = cursor.getValue();
2664 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2665 && value.hasIdentity("conference", "text")
2666 && !value.getFeatures().contains("jabber:iq:gateway")
2667 && !value.hasIdentity("conference", "irc")) {
2668 servers.add(cursor.getKey().toString());
2669 }
2670 }
2671 }
2672 return servers;
2673 }
2674
2675 public String getMucServer() {
2676 List<String> servers = getMucServers();
2677 return servers.size() > 0 ? servers.get(0) : null;
2678 }
2679
2680 public int getTimeToNextAttempt(final boolean aggressive) {
2681 final int interval;
2682 if (aggressive) {
2683 interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2684 } else {
2685 final int additionalTime =
2686 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2687 interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2688 }
2689 final int secondsSinceLast =
2690 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2691 return interval - secondsSinceLast;
2692 }
2693
2694 public int getAttempt() {
2695 return this.attempt;
2696 }
2697
2698 public Features getFeatures() {
2699 return this.features;
2700 }
2701
2702 public long getLastSessionEstablished() {
2703 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2704 return System.currentTimeMillis() - diff;
2705 }
2706
2707 public long getLastConnect() {
2708 return this.lastConnect;
2709 }
2710
2711 public long getLastPingSent() {
2712 return this.lastPingSent;
2713 }
2714
2715 public long getLastDiscoStarted() {
2716 return this.lastDiscoStarted;
2717 }
2718
2719 public long getLastPacketReceived() {
2720 return this.lastPacketReceived;
2721 }
2722
2723 public void sendActive() {
2724 this.sendPacket(new Active());
2725 }
2726
2727 public void sendInactive() {
2728 this.sendPacket(new Inactive());
2729 }
2730
2731 public void resetAttemptCount(boolean resetConnectTime) {
2732 this.attempt = 0;
2733 if (resetConnectTime) {
2734 this.lastConnect = 0;
2735 }
2736 }
2737
2738 public void setInteractive(boolean interactive) {
2739 this.mInteractive = interactive;
2740 }
2741
2742 private IqGenerator getIqGenerator() {
2743 return mXmppConnectionService.getIqGenerator();
2744 }
2745
2746 public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
2747 if (trackOfflineMessageRetrieval) {
2748 final Iq iqPing = new Iq(Iq.Type.GET);
2749 iqPing.addChild("ping", Namespace.PING);
2750 this.sendIqPacket(
2751 iqPing,
2752 (response) -> {
2753 Log.d(
2754 Config.LOGTAG,
2755 account.getJid().asBareJid()
2756 + ": got ping response after sending initial presence");
2757 XmppConnection.this.offlineMessagesRetrieved = true;
2758 });
2759 } else {
2760 this.offlineMessagesRetrieved = true;
2761 }
2762 }
2763
2764 public boolean isOfflineMessagesRetrieved() {
2765 return this.offlineMessagesRetrieved;
2766 }
2767
2768 public void fetchRoster() {
2769 final Iq iqPacket = new Iq(Iq.Type.GET);
2770 final var version = account.getRosterVersion();
2771 if (Strings.isNullOrEmpty(account.getRosterVersion())) {
2772 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
2773 } else {
2774 Log.d(
2775 Config.LOGTAG,
2776 account.getJid().asBareJid() + ": fetching roster version " + version);
2777 }
2778 iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
2779 sendIqPacket(iqPacket, unregisteredIqListener);
2780 }
2781
2782 private class MyKeyManager implements X509KeyManager {
2783 @Override
2784 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2785 return account.getPrivateKeyAlias();
2786 }
2787
2788 @Override
2789 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2790 return null;
2791 }
2792
2793 @Override
2794 public X509Certificate[] getCertificateChain(String alias) {
2795 Log.d(Config.LOGTAG, "getting certificate chain");
2796 try {
2797 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2798 } catch (final Exception e) {
2799 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2800 return new X509Certificate[0];
2801 }
2802 }
2803
2804 @Override
2805 public String[] getClientAliases(String s, Principal[] principals) {
2806 final String alias = account.getPrivateKeyAlias();
2807 return alias != null ? new String[] {alias} : new String[0];
2808 }
2809
2810 @Override
2811 public String[] getServerAliases(String s, Principal[] principals) {
2812 return new String[0];
2813 }
2814
2815 @Override
2816 public PrivateKey getPrivateKey(String alias) {
2817 try {
2818 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2819 } catch (Exception e) {
2820 return null;
2821 }
2822 }
2823 }
2824
2825 private static class LoginInfo {
2826 public final SaslMechanism saslMechanism;
2827 public final SaslMechanism.Version saslVersion;
2828 public final List<String> inlineBindFeatures;
2829 public final AtomicBoolean success = new AtomicBoolean(false);
2830
2831 private LoginInfo(
2832 final SaslMechanism saslMechanism,
2833 final SaslMechanism.Version saslVersion,
2834 final Collection<String> inlineBindFeatures) {
2835 Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
2836 Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
2837 this.saslMechanism = saslMechanism;
2838 this.saslVersion = saslVersion;
2839 this.inlineBindFeatures =
2840 inlineBindFeatures == null
2841 ? Collections.emptyList()
2842 : ImmutableList.copyOf(inlineBindFeatures);
2843 }
2844
2845 public static SaslMechanism mechanism(final LoginInfo loginInfo) {
2846 return loginInfo == null ? null : loginInfo.saslMechanism;
2847 }
2848
2849 public void success(final String challenge, final SSLSocket sslSocket)
2850 throws SaslMechanism.AuthenticationException {
2851 final var response = this.saslMechanism.getResponse(challenge, sslSocket);
2852 if (!Strings.isNullOrEmpty(response)) {
2853 throw new SaslMechanism.AuthenticationException(
2854 "processing success yielded another response");
2855 }
2856 if (this.success.compareAndSet(false, true)) {
2857 return;
2858 }
2859 throw new SaslMechanism.AuthenticationException("Process 'success' twice");
2860 }
2861
2862 public static boolean isSuccess(final LoginInfo loginInfo) {
2863 return loginInfo != null && loginInfo.success.get();
2864 }
2865 }
2866
2867 private static class StreamId {
2868 public final String id;
2869 public final Resolver.Result location;
2870
2871 private StreamId(String id, Resolver.Result location) {
2872 this.id = id;
2873 this.location = location;
2874 }
2875
2876 @NonNull
2877 @Override
2878 public String toString() {
2879 return MoreObjects.toStringHelper(this)
2880 .add("id", id)
2881 .add("location", location)
2882 .toString();
2883 }
2884 }
2885
2886 private static class StateChangingError extends Error {
2887 private final Account.State state;
2888
2889 public StateChangingError(Account.State state) {
2890 this.state = state;
2891 }
2892 }
2893
2894 private static class StateChangingException extends IOException {
2895 private final Account.State state;
2896
2897 public StateChangingException(Account.State state) {
2898 this.state = state;
2899 }
2900 }
2901
2902 public class Features {
2903 XmppConnection connection;
2904 private boolean carbonsEnabled = false;
2905 private boolean encryptionEnabled = false;
2906 private boolean blockListRequested = false;
2907
2908 public Features(final XmppConnection connection) {
2909 this.connection = connection;
2910 }
2911
2912 private boolean hasDiscoFeature(final Jid server, final String feature) {
2913 synchronized (XmppConnection.this.disco) {
2914 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2915 return sdr != null && sdr.getFeatures().contains(feature);
2916 }
2917 }
2918
2919 public boolean carbons() {
2920 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2921 }
2922
2923 public boolean commands() {
2924 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2925 }
2926
2927 public boolean easyOnboardingInvites() {
2928 synchronized (commands) {
2929 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2930 }
2931 }
2932
2933 public boolean bookmarksConversion() {
2934 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2935 && pepPublishOptions();
2936 }
2937
2938 public boolean blocking() {
2939 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2940 }
2941
2942 public boolean spamReporting() {
2943 return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
2944 }
2945
2946 public boolean flexibleOfflineMessageRetrieval() {
2947 return hasDiscoFeature(
2948 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2949 }
2950
2951 public boolean register() {
2952 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2953 }
2954
2955 public boolean invite() {
2956 return connection.streamFeatures != null
2957 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2958 }
2959
2960 public boolean sm() {
2961 return streamId != null
2962 || (connection.streamFeatures != null
2963 && connection.streamFeatures.streamManagement());
2964 }
2965
2966 public boolean csi() {
2967 return connection.streamFeatures != null
2968 && connection.streamFeatures.clientStateIndication();
2969 }
2970
2971 public boolean pep() {
2972 synchronized (XmppConnection.this.disco) {
2973 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2974 return info != null && info.hasIdentity("pubsub", "pep");
2975 }
2976 }
2977
2978 public boolean pepPersistent() {
2979 synchronized (XmppConnection.this.disco) {
2980 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2981 return info != null
2982 && info.getFeatures()
2983 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2984 }
2985 }
2986
2987 public boolean bind2() {
2988 final var loginInfo = XmppConnection.this.loginInfo;
2989 return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty();
2990 }
2991
2992 public boolean sasl2() {
2993 final var loginInfo = XmppConnection.this.loginInfo;
2994 return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2;
2995 }
2996
2997 public String loginMechanism() {
2998 final var loginInfo = XmppConnection.this.loginInfo;
2999 return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism();
3000 }
3001
3002 public boolean pepPublishOptions() {
3003 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
3004 }
3005
3006 public boolean pepConfigNodeMax() {
3007 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX);
3008 }
3009
3010 public boolean pepOmemoWhitelisted() {
3011 return hasDiscoFeature(
3012 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
3013 }
3014
3015 public boolean mam() {
3016 return MessageArchiveService.Version.has(getAccountFeatures());
3017 }
3018
3019 public List<String> getAccountFeatures() {
3020 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
3021 return result == null ? Collections.emptyList() : result.getFeatures();
3022 }
3023
3024 public boolean push() {
3025 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
3026 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
3027 }
3028
3029 public boolean rosterVersioning() {
3030 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
3031 }
3032
3033 public void setBlockListRequested(boolean value) {
3034 this.blockListRequested = value;
3035 }
3036
3037 public boolean httpUpload(long filesize) {
3038 if (Config.DISABLE_HTTP_UPLOAD) {
3039 return false;
3040 } else {
3041 for (String namespace :
3042 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
3043 List<Entry<Jid, ServiceDiscoveryResult>> items =
3044 findDiscoItemsByFeature(namespace);
3045 if (items.size() > 0) {
3046 try {
3047 long maxsize =
3048 Long.parseLong(
3049 items.get(0)
3050 .getValue()
3051 .getExtendedDiscoInformation(
3052 namespace, "max-file-size"));
3053 if (filesize <= maxsize) {
3054 return true;
3055 } else {
3056 Log.d(
3057 Config.LOGTAG,
3058 account.getJid().asBareJid()
3059 + ": http upload is not available for files with size "
3060 + filesize
3061 + " (max is "
3062 + maxsize
3063 + ")");
3064 return false;
3065 }
3066 } catch (Exception e) {
3067 return true;
3068 }
3069 }
3070 }
3071 return false;
3072 }
3073 }
3074
3075 public boolean useLegacyHttpUpload() {
3076 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
3077 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
3078 }
3079
3080 public long getMaxHttpUploadSize() {
3081 for (String namespace :
3082 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
3083 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
3084 if (items.size() > 0) {
3085 try {
3086 return Long.parseLong(
3087 items.get(0)
3088 .getValue()
3089 .getExtendedDiscoInformation(namespace, "max-file-size"));
3090 } catch (Exception e) {
3091 // ignored
3092 }
3093 }
3094 }
3095 return -1;
3096 }
3097
3098 public boolean stanzaIds() {
3099 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3100 }
3101
3102 public boolean bookmarks2() {
3103 return pepPublishOptions()
3104 && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3105 }
3106
3107 public boolean externalServiceDiscovery() {
3108 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3109 }
3110
3111 public boolean mds() {
3112 return pepPublishOptions()
3113 && pepConfigNodeMax()
3114 && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION;
3115 }
3116
3117 public boolean mdsServerAssist() {
3118 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED);
3119 }
3120 }
3121}