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