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 && acknowledgedListener != null) {
1166 final MessagePacket packet = (MessagePacket) stanza;
1167 final String id = packet.getId();
1168 final Jid to = packet.getTo();
1169 if (id != null && to != null) {
1170 acknowledgedMessages |=
1171 acknowledgedListener.onMessageAcknowledged(account, to, id);
1172 }
1173 }
1174 mStanzaQueue.removeAt(i);
1175 i--;
1176 }
1177 }
1178 return acknowledgedMessages;
1179 }
1180
1181 private @NonNull Element processPacket(final Tag currentTag, final int packetType)
1182 throws IOException {
1183 final Element element =
1184 switch (packetType) {
1185 case PACKET_IQ -> new IqPacket();
1186 case PACKET_MESSAGE -> new MessagePacket();
1187 case PACKET_PRESENCE -> new PresencePacket();
1188 default -> throw new AssertionError("Should never encounter invalid type");
1189 };
1190 element.setAttributes(currentTag.getAttributes());
1191 Tag nextTag = tagReader.readTag();
1192 if (nextTag == null) {
1193 throw new IOException("interrupted mid tag");
1194 }
1195 while (!nextTag.isEnd(element.getName())) {
1196 if (!nextTag.isNo()) {
1197 element.addChild(tagReader.readElement(nextTag));
1198 }
1199 nextTag = tagReader.readTag();
1200 if (nextTag == null) {
1201 throw new IOException("interrupted mid tag");
1202 }
1203 }
1204 if (stanzasReceived == Integer.MAX_VALUE) {
1205 resetStreamId();
1206 throw new IOException("time to restart the session. cant handle >2 billion pcks");
1207 }
1208 if (inSmacksSession) {
1209 ++stanzasReceived;
1210 } else if (features.sm()) {
1211 Log.d(
1212 Config.LOGTAG,
1213 account.getJid().asBareJid()
1214 + ": not counting stanza("
1215 + element.getClass().getSimpleName()
1216 + "). Not in smacks session.");
1217 }
1218 lastPacketReceived = SystemClock.elapsedRealtime();
1219 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
1220 Log.d(Config.LOGTAG, "[background stanza] " + element);
1221 }
1222 if (element instanceof IqPacket
1223 && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
1224 && element.hasChild("jingle", Namespace.JINGLE)) {
1225 return JinglePacket.upgrade((IqPacket) element);
1226 } else {
1227 return element;
1228 }
1229 }
1230
1231 private void processIq(final Tag currentTag) throws IOException {
1232 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1233 if (!packet.valid()) {
1234 Log.e(
1235 Config.LOGTAG,
1236 "encountered invalid iq from='"
1237 + packet.getFrom()
1238 + "' to='"
1239 + packet.getTo()
1240 + "'");
1241 return;
1242 }
1243 if (Thread.currentThread().isInterrupted()) {
1244 Log.d(
1245 Config.LOGTAG,
1246 account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
1247 return;
1248 }
1249 if (packet instanceof JinglePacket jinglePacket && isBound) {
1250 if (this.jingleListener != null) {
1251 this.jingleListener.onJinglePacketReceived(account, jinglePacket);
1252 }
1253 } else {
1254 final OnIqPacketReceived callback = getIqPacketReceivedCallback(packet);
1255 if (callback == null) {
1256 Log.d(
1257 Config.LOGTAG,
1258 account.getJid().asBareJid().toString()
1259 + ": no callback registered for IQ from "
1260 + packet.getFrom());
1261 return;
1262 }
1263 try {
1264 callback.onIqPacketReceived(account, packet);
1265 } catch (final StateChangingError error) {
1266 throw new StateChangingException(error.state);
1267 }
1268 }
1269 }
1270
1271 private OnIqPacketReceived getIqPacketReceivedCallback(final IqPacket stanza)
1272 throws StateChangingException {
1273 final boolean isRequest =
1274 stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET;
1275 if (isRequest) {
1276 if (isBound) {
1277 return this.unregisteredIqListener;
1278 } else {
1279 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1280 }
1281 } else {
1282 synchronized (this.packetCallbacks) {
1283 final var pair = packetCallbacks.get(stanza.getId());
1284 if (pair == null) {
1285 return null;
1286 }
1287 if (pair.first.toServer(account)) {
1288 if (stanza.fromServer(account)) {
1289 packetCallbacks.remove(stanza.getId());
1290 return pair.second;
1291 } else {
1292 Log.e(
1293 Config.LOGTAG,
1294 account.getJid().asBareJid().toString()
1295 + ": ignoring spoofed iq packet");
1296 }
1297 } else {
1298 if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
1299 packetCallbacks.remove(stanza.getId());
1300 return pair.second;
1301 } else {
1302 Log.e(
1303 Config.LOGTAG,
1304 account.getJid().asBareJid().toString()
1305 + ": ignoring spoofed iq packet");
1306 }
1307 }
1308 }
1309 }
1310 return null;
1311 }
1312
1313 private void processMessage(final Tag currentTag) throws IOException {
1314 final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1315 if (!packet.valid()) {
1316 Log.e(
1317 Config.LOGTAG,
1318 "encountered invalid message from='"
1319 + packet.getFrom()
1320 + "' to='"
1321 + packet.getTo()
1322 + "'");
1323 return;
1324 }
1325 if (Thread.currentThread().isInterrupted()) {
1326 Log.d(
1327 Config.LOGTAG,
1328 account.getJid().asBareJid()
1329 + "Not processing message. Thread was interrupted");
1330 return;
1331 }
1332 this.messageListener.onMessagePacketReceived(account, packet);
1333 }
1334
1335 private void processPresence(final Tag currentTag) throws IOException {
1336 final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1337 if (!packet.valid()) {
1338 Log.e(
1339 Config.LOGTAG,
1340 "encountered invalid presence from='"
1341 + packet.getFrom()
1342 + "' to='"
1343 + packet.getTo()
1344 + "'");
1345 return;
1346 }
1347 if (Thread.currentThread().isInterrupted()) {
1348 Log.d(
1349 Config.LOGTAG,
1350 account.getJid().asBareJid()
1351 + "Not processing presence. Thread was interrupted");
1352 return;
1353 }
1354 this.presenceListener.onPresencePacketReceived(account, packet);
1355 }
1356
1357 private void sendStartTLS() throws IOException {
1358 final Tag startTLS = Tag.empty("starttls");
1359 startTLS.setAttribute("xmlns", Namespace.TLS);
1360 tagWriter.writeTag(startTLS);
1361 }
1362
1363 private void switchOverToTls() throws XmlPullParserException, IOException {
1364 tagReader.readTag();
1365 final Socket socket = this.socket;
1366 final SSLSocket sslSocket = upgradeSocketToTls(socket);
1367 this.socket = sslSocket;
1368 this.tagReader.setInputStream(sslSocket.getInputStream());
1369 this.tagWriter.setOutputStream(sslSocket.getOutputStream());
1370 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1371 final boolean quickStart;
1372 try {
1373 quickStart = establishStream(SSLSockets.version(sslSocket));
1374 } catch (final InterruptedException e) {
1375 return;
1376 }
1377 if (quickStart) {
1378 this.quickStartInProgress = true;
1379 }
1380 features.encryptionEnabled = true;
1381 final Tag tag = tagReader.readTag();
1382 if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
1383 SSLSockets.log(account, sslSocket);
1384 processStream();
1385 } else {
1386 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1387 }
1388 sslSocket.close();
1389 }
1390
1391 private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1392 final SSLSocketFactory sslSocketFactory;
1393 try {
1394 sslSocketFactory = getSSLSocketFactory();
1395 } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1396 throw new StateChangingException(Account.State.TLS_ERROR);
1397 }
1398 final InetAddress address = socket.getInetAddress();
1399 final SSLSocket sslSocket =
1400 (SSLSocket)
1401 sslSocketFactory.createSocket(
1402 socket, address.getHostAddress(), socket.getPort(), true);
1403 SSLSockets.setSecurity(sslSocket);
1404 SSLSockets.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1405 SSLSockets.setApplicationProtocol(sslSocket, "xmpp-client");
1406 final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1407 try {
1408 if (!xmppDomainVerifier.verify(
1409 account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1410 Log.d(
1411 Config.LOGTAG,
1412 account.getJid().asBareJid()
1413 + ": TLS certificate domain verification failed");
1414 FileBackend.close(sslSocket);
1415 throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1416 }
1417 } catch (final SSLPeerUnverifiedException e) {
1418 FileBackend.close(sslSocket);
1419 throw new StateChangingException(Account.State.TLS_ERROR);
1420 }
1421 return sslSocket;
1422 }
1423
1424 private void processStreamFeatures(final Tag currentTag) throws IOException {
1425 this.streamFeatures = tagReader.readElement(currentTag);
1426 final boolean isSecure = isSecure();
1427 final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1428 if (this.quickStartInProgress) {
1429 if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1430 Log.d(
1431 Config.LOGTAG,
1432 account.getJid().asBareJid()
1433 + ": quick start in progress. ignoring features: "
1434 + XmlHelper.printElementNames(this.streamFeatures));
1435 if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
1436 return;
1437 }
1438 if (isFastTokenAvailable(
1439 this.streamFeatures.findChild("authentication", Namespace.SASL_2))) {
1440 Log.d(
1441 Config.LOGTAG,
1442 account.getJid().asBareJid()
1443 + ": fast token available; resetting quick start");
1444 account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1445 mXmppConnectionService.databaseBackend.updateAccount(account);
1446 }
1447 return;
1448 }
1449 Log.d(
1450 Config.LOGTAG,
1451 account.getJid().asBareJid()
1452 + ": server lost support for SASL 2. quick start not possible");
1453 this.account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false);
1454 mXmppConnectionService.databaseBackend.updateAccount(account);
1455 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1456 }
1457 if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1458 && !features.encryptionEnabled) {
1459 sendStartTLS();
1460 } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1461 && account.isOptionSet(Account.OPTION_REGISTER)) {
1462 if (isSecure) {
1463 register();
1464 } else {
1465 Log.d(
1466 Config.LOGTAG,
1467 account.getJid().asBareJid()
1468 + ": unable to find STARTTLS for registration process "
1469 + XmlHelper.printElementNames(this.streamFeatures));
1470 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1471 }
1472 } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1473 && account.isOptionSet(Account.OPTION_REGISTER)) {
1474 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1475 } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
1476 && shouldAuthenticate
1477 && isSecure) {
1478 authenticate(SaslMechanism.Version.SASL_2);
1479 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1480 && shouldAuthenticate
1481 && isSecure) {
1482 authenticate(SaslMechanism.Version.SASL);
1483 } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1484 && isSecure
1485 && loginInfo != null
1486 && streamId != null
1487 && !inSmacksSession) {
1488 if (Config.EXTENDED_SM_LOGGING) {
1489 Log.d(
1490 Config.LOGTAG,
1491 account.getJid().asBareJid()
1492 + ": resuming after stanza #"
1493 + stanzasReceived);
1494 }
1495 final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
1496 this.mSmCatchupMessageCounter.set(0);
1497 this.mWaitingForSmCatchup.set(true);
1498 this.tagWriter.writeStanzaAsync(resume);
1499 } else if (needsBinding) {
1500 if (this.streamFeatures.hasChild("bind", Namespace.BIND)
1501 && isSecure
1502 && loginInfo != null) {
1503 sendBindRequest();
1504 } else {
1505 Log.d(
1506 Config.LOGTAG,
1507 account.getJid().asBareJid()
1508 + ": unable to find bind feature "
1509 + XmlHelper.printElementNames(this.streamFeatures));
1510 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1511 }
1512 } else {
1513 Log.d(
1514 Config.LOGTAG,
1515 account.getJid().asBareJid()
1516 + ": received NOP stream features: "
1517 + XmlHelper.printElementNames(this.streamFeatures));
1518 }
1519 }
1520
1521 private void authenticate() throws IOException {
1522 final boolean isSecure = isSecure();
1523 if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
1524 authenticate(SaslMechanism.Version.SASL_2);
1525 } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
1526 authenticate(SaslMechanism.Version.SASL);
1527 } else {
1528 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1529 }
1530 }
1531
1532 private boolean isSecure() {
1533 return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1534 }
1535
1536 private void authenticate(final SaslMechanism.Version version) throws IOException {
1537 final Element authElement;
1538 if (version == SaslMechanism.Version.SASL) {
1539 authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
1540 } else {
1541 authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
1542 }
1543 final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
1544 final Element cbElement =
1545 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
1546 final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
1547 final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
1548 final SaslMechanism saslMechanism =
1549 factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
1550 this.validate(saslMechanism, mechanisms);
1551 final boolean quickStartAvailable;
1552 final String firstMessage =
1553 saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
1554 final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
1555 final Element authenticate;
1556 if (version == SaslMechanism.Version.SASL) {
1557 authenticate = new Element("auth", Namespace.SASL);
1558 if (!Strings.isNullOrEmpty(firstMessage)) {
1559 authenticate.setContent(firstMessage);
1560 }
1561 quickStartAvailable = false;
1562 this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
1563 } else if (version == SaslMechanism.Version.SASL_2) {
1564 final Element inline = authElement.findChild("inline", Namespace.SASL_2);
1565 final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1566 final HashedToken.Mechanism hashTokenRequest;
1567 if (usingFast) {
1568 hashTokenRequest = null;
1569 } else {
1570 final Element fast =
1571 inline == null ? null : inline.findChild("fast", Namespace.FAST);
1572 final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
1573 hashTokenRequest =
1574 HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
1575 }
1576 final Collection<String> bindFeatures = Bind2.features(inline);
1577 quickStartAvailable =
1578 sm
1579 && bindFeatures != null
1580 && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
1581 if (bindFeatures != null) {
1582 try {
1583 mXmppConnectionService.restoredFromDatabaseLatch.await();
1584 } catch (final InterruptedException e) {
1585 Log.d(
1586 Config.LOGTAG,
1587 account.getJid().asBareJid()
1588 + ": interrupted while waiting for DB restore during SASL2 bind");
1589 return;
1590 }
1591 }
1592 this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
1593 this.hashTokenRequest = hashTokenRequest;
1594 authenticate =
1595 generateAuthenticationRequest(
1596 firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
1597 } else {
1598 throw new AssertionError("Missing implementation for " + version);
1599 }
1600
1601 if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
1602 mXmppConnectionService.databaseBackend.updateAccount(account);
1603 }
1604
1605 Log.d(
1606 Config.LOGTAG,
1607 account.getJid().toString()
1608 + ": Authenticating with "
1609 + version
1610 + "/"
1611 + LoginInfo.mechanism(this.loginInfo).getMechanism());
1612 authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism());
1613 synchronized (this.mStanzaQueue) {
1614 this.stanzasSentBeforeAuthentication = this.stanzasSent;
1615 tagWriter.writeElement(authenticate);
1616 }
1617 }
1618
1619 private static boolean isFastTokenAvailable(final Element authentication) {
1620 final Element inline = authentication == null ? null : authentication.findChild("inline");
1621 return inline != null && inline.hasChild("fast", Namespace.FAST);
1622 }
1623
1624 private void validate(
1625 final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms)
1626 throws StateChangingException {
1627 if (saslMechanism == null) {
1628 Log.d(
1629 Config.LOGTAG,
1630 account.getJid().asBareJid()
1631 + ": unable to find supported SASL mechanism in "
1632 + mechanisms);
1633 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1634 }
1635 if (SaslMechanism.hashedToken(saslMechanism)) {
1636 return;
1637 }
1638 final int pinnedMechanism = account.getPinnedMechanismPriority();
1639 if (pinnedMechanism > saslMechanism.getPriority()) {
1640 Log.e(
1641 Config.LOGTAG,
1642 "Auth failed. Authentication mechanism "
1643 + saslMechanism.getMechanism()
1644 + " has lower priority ("
1645 + saslMechanism.getPriority()
1646 + ") than pinned priority ("
1647 + pinnedMechanism
1648 + "). Possible downgrade attack?");
1649 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1650 }
1651 }
1652
1653 private Element generateAuthenticationRequest(
1654 final String firstMessage, final boolean usingFast) {
1655 return generateAuthenticationRequest(
1656 firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
1657 }
1658
1659 private Element generateAuthenticationRequest(
1660 final String firstMessage,
1661 final boolean usingFast,
1662 final HashedToken.Mechanism hashedTokenRequest,
1663 final Collection<String> bind,
1664 final boolean inlineStreamManagement) {
1665 final Element authenticate = new Element("authenticate", Namespace.SASL_2);
1666 if (!Strings.isNullOrEmpty(firstMessage)) {
1667 authenticate.addChild("initial-response").setContent(firstMessage);
1668 }
1669 final Element userAgent = authenticate.addChild("user-agent");
1670 userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
1671 userAgent
1672 .addChild("software")
1673 .setContent(mXmppConnectionService.getString(R.string.app_name));
1674 if (!PhoneHelper.isEmulator()) {
1675 userAgent
1676 .addChild("device")
1677 .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
1678 }
1679 // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
1680 // (because we would rather just do a normal SM/resume)
1681 final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
1682 if (bind != null && mayAttemptBind) {
1683 authenticate.addChild(generateBindRequest(bind));
1684 }
1685 if (inlineStreamManagement && streamId != null) {
1686 final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
1687 this.mSmCatchupMessageCounter.set(0);
1688 this.mWaitingForSmCatchup.set(true);
1689 authenticate.addChild(resume);
1690 }
1691 if (hashedTokenRequest != null) {
1692 authenticate
1693 .addChild("request-token", Namespace.FAST)
1694 .setAttribute("mechanism", hashedTokenRequest.name());
1695 }
1696 if (usingFast) {
1697 authenticate.addChild("fast", Namespace.FAST);
1698 }
1699 return authenticate;
1700 }
1701
1702 private Element generateBindRequest(final Collection<String> bindFeatures) {
1703 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1704 final Element bind = new Element("bind", Namespace.BIND2);
1705 bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
1706 if (bindFeatures.contains(Namespace.CARBONS)) {
1707 bind.addChild("enable", Namespace.CARBONS);
1708 }
1709 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1710 bind.addChild(new EnablePacket());
1711 }
1712 return bind;
1713 }
1714
1715 private void register() {
1716 final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
1717 if (preAuth != null && features.invite()) {
1718 final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1719 preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1720 sendUnmodifiedIqPacket(
1721 preAuthRequest,
1722 (account, response) -> {
1723 if (response.getType() == IqPacket.TYPE.RESULT) {
1724 sendRegistryRequest();
1725 } else {
1726 final String error = response.getErrorCondition();
1727 Log.d(
1728 Config.LOGTAG,
1729 account.getJid().asBareJid()
1730 + ": failed to pre auth. "
1731 + error);
1732 throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1733 }
1734 },
1735 true);
1736 } else {
1737 sendRegistryRequest();
1738 }
1739 }
1740
1741 private void sendRegistryRequest() {
1742 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1743 register.query(Namespace.REGISTER);
1744 register.setTo(account.getDomain());
1745 sendUnmodifiedIqPacket(
1746 register,
1747 (account, packet) -> {
1748 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1749 return;
1750 }
1751 if (packet.getType() == IqPacket.TYPE.ERROR) {
1752 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1753 }
1754 final Element query = packet.query(Namespace.REGISTER);
1755 if (query.hasChild("username") && (query.hasChild("password"))) {
1756 final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1757 final Element username =
1758 new Element("username").setContent(account.getUsername());
1759 final Element password =
1760 new Element("password").setContent(account.getPassword());
1761 register1.query(Namespace.REGISTER).addChild(username);
1762 register1.query().addChild(password);
1763 register1.setFrom(account.getJid().asBareJid());
1764 sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1765 } else if (query.hasChild("x", Namespace.DATA)) {
1766 final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1767 final Element blob = query.findChild("data", "urn:xmpp:bob");
1768 final String id = packet.getId();
1769 InputStream is;
1770 if (blob != null) {
1771 try {
1772 final String base64Blob = blob.getContent();
1773 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1774 is = new ByteArrayInputStream(strBlob);
1775 } catch (Exception e) {
1776 is = null;
1777 }
1778 } else {
1779 final boolean useTor =
1780 mXmppConnectionService.useTorToConnect() || account.isOnion();
1781 try {
1782 final String url = data.getValue("url");
1783 final String fallbackUrl = data.getValue("captcha-fallback-url");
1784 if (url != null) {
1785 is = HttpConnectionManager.open(url, useTor);
1786 } else if (fallbackUrl != null) {
1787 is = HttpConnectionManager.open(fallbackUrl, useTor);
1788 } else {
1789 is = null;
1790 }
1791 } catch (final IOException e) {
1792 Log.d(
1793 Config.LOGTAG,
1794 account.getJid().asBareJid() + ": unable to fetch captcha",
1795 e);
1796 is = null;
1797 }
1798 }
1799
1800 if (is != null) {
1801 Bitmap captcha = BitmapFactory.decodeStream(is);
1802 try {
1803 if (mXmppConnectionService.displayCaptchaRequest(
1804 account, id, data, captcha)) {
1805 return;
1806 }
1807 } catch (Exception e) {
1808 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1809 }
1810 }
1811 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1812 } else if (query.hasChild("instructions")
1813 || query.hasChild("x", Namespace.OOB)) {
1814 final String instructions = query.findChildContent("instructions");
1815 final Element oob = query.findChild("x", Namespace.OOB);
1816 final String url = oob == null ? null : oob.findChildContent("url");
1817 if (url != null) {
1818 setAccountCreationFailed(url);
1819 } else if (instructions != null) {
1820 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1821 if (matcher.find()) {
1822 setAccountCreationFailed(
1823 instructions.substring(matcher.start(), matcher.end()));
1824 }
1825 }
1826 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1827 }
1828 },
1829 true);
1830 }
1831
1832 private void setAccountCreationFailed(final String url) {
1833 final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1834 if (httpUrl != null && httpUrl.isHttps()) {
1835 this.redirectionUrl = httpUrl;
1836 throw new StateChangingError(Account.State.REGISTRATION_WEB);
1837 }
1838 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1839 }
1840
1841 public HttpUrl getRedirectionUrl() {
1842 return this.redirectionUrl;
1843 }
1844
1845 public void resetEverything() {
1846 resetAttemptCount(true);
1847 resetStreamId();
1848 clearIqCallbacks();
1849 this.stanzasSent = 0;
1850 mStanzaQueue.clear();
1851 this.redirectionUrl = null;
1852 synchronized (this.disco) {
1853 disco.clear();
1854 }
1855 synchronized (this.commands) {
1856 this.commands.clear();
1857 }
1858 this.loginInfo = null;
1859 }
1860
1861 private void sendBindRequest() {
1862 try {
1863 mXmppConnectionService.restoredFromDatabaseLatch.await();
1864 } catch (InterruptedException e) {
1865 Log.d(
1866 Config.LOGTAG,
1867 account.getJid().asBareJid()
1868 + ": interrupted while waiting for DB restore during bind");
1869 return;
1870 }
1871 clearIqCallbacks();
1872 if (account.getJid().isBareJid()) {
1873 account.setResource(this.createNewResource());
1874 } else {
1875 fixResource(mXmppConnectionService, account);
1876 }
1877 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1878 final String resource =
1879 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1880 iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1881 this.sendUnmodifiedIqPacket(
1882 iq,
1883 (account, packet) -> {
1884 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1885 return;
1886 }
1887 final Element bind = packet.findChild("bind");
1888 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1889 isBound = true;
1890 final Element jid = bind.findChild("jid");
1891 if (jid != null && jid.getContent() != null) {
1892 try {
1893 Jid assignedJid = Jid.ofEscaped(jid.getContent());
1894 if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1895 Log.d(
1896 Config.LOGTAG,
1897 account.getJid().asBareJid()
1898 + ": server tried to re-assign domain to "
1899 + assignedJid.getDomain());
1900 throw new StateChangingError(Account.State.BIND_FAILURE);
1901 }
1902 if (account.setJid(assignedJid)) {
1903 Log.d(
1904 Config.LOGTAG,
1905 account.getJid().asBareJid()
1906 + ": jid changed during bind. updating database");
1907 mXmppConnectionService.databaseBackend.updateAccount(account);
1908 }
1909 if (streamFeatures.hasChild("session")
1910 && !streamFeatures
1911 .findChild("session")
1912 .hasChild("optional")) {
1913 sendStartSession();
1914 } else {
1915 final boolean waitForDisco = enableStreamManagement();
1916 sendPostBindInitialization(waitForDisco, false);
1917 }
1918 return;
1919 } catch (final IllegalArgumentException e) {
1920 Log.d(
1921 Config.LOGTAG,
1922 account.getJid().asBareJid()
1923 + ": server reported invalid jid ("
1924 + jid.getContent()
1925 + ") on bind");
1926 }
1927 } else {
1928 Log.d(
1929 Config.LOGTAG,
1930 account.getJid()
1931 + ": disconnecting because of bind failure. (no jid)");
1932 }
1933 } else {
1934 Log.d(
1935 Config.LOGTAG,
1936 account.getJid()
1937 + ": disconnecting because of bind failure ("
1938 + packet);
1939 }
1940 final Element error = packet.findChild("error");
1941 if (packet.getType() == IqPacket.TYPE.ERROR
1942 && error != null
1943 && error.hasChild("conflict")) {
1944 account.setResource(createNewResource());
1945 }
1946 throw new StateChangingError(Account.State.BIND_FAILURE);
1947 },
1948 true);
1949 }
1950
1951 private void clearIqCallbacks() {
1952 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1953 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1954 synchronized (this.packetCallbacks) {
1955 if (this.packetCallbacks.size() == 0) {
1956 return;
1957 }
1958 Log.d(
1959 Config.LOGTAG,
1960 account.getJid().asBareJid()
1961 + ": clearing "
1962 + this.packetCallbacks.size()
1963 + " iq callbacks");
1964 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1965 this.packetCallbacks.values().iterator();
1966 while (iterator.hasNext()) {
1967 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1968 callbacks.add(entry.second);
1969 iterator.remove();
1970 }
1971 }
1972 for (OnIqPacketReceived callback : callbacks) {
1973 try {
1974 callback.onIqPacketReceived(account, failurePacket);
1975 } catch (StateChangingError error) {
1976 Log.d(
1977 Config.LOGTAG,
1978 account.getJid().asBareJid()
1979 + ": caught StateChangingError("
1980 + error.state.toString()
1981 + ") while clearing callbacks");
1982 // ignore
1983 }
1984 }
1985 Log.d(
1986 Config.LOGTAG,
1987 account.getJid().asBareJid()
1988 + ": done clearing iq callbacks. "
1989 + this.packetCallbacks.size()
1990 + " left");
1991 }
1992
1993 public void sendDiscoTimeout() {
1994 if (mWaitForDisco.compareAndSet(true, false)) {
1995 Log.d(
1996 Config.LOGTAG,
1997 account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1998 finalizeBind();
1999 }
2000 }
2001
2002 private void sendStartSession() {
2003 Log.d(
2004 Config.LOGTAG,
2005 account.getJid().asBareJid() + ": sending legacy session to outdated server");
2006 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
2007 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
2008 this.sendUnmodifiedIqPacket(
2009 startSession,
2010 (account, packet) -> {
2011 if (packet.getType() == IqPacket.TYPE.RESULT) {
2012 final boolean waitForDisco = enableStreamManagement();
2013 sendPostBindInitialization(waitForDisco, false);
2014 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2015 throw new StateChangingError(Account.State.SESSION_FAILURE);
2016 }
2017 },
2018 true);
2019 }
2020
2021 private boolean enableStreamManagement() {
2022 final boolean streamManagement =
2023 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
2024 if (streamManagement) {
2025 synchronized (this.mStanzaQueue) {
2026 final EnablePacket enable = new EnablePacket();
2027 tagWriter.writeStanzaAsync(enable);
2028 stanzasSent = 0;
2029 mStanzaQueue.clear();
2030 }
2031 return true;
2032 } else {
2033 return false;
2034 }
2035 }
2036
2037 private void sendPostBindInitialization(
2038 final boolean waitForDisco, final boolean carbonsEnabled) {
2039 features.carbonsEnabled = carbonsEnabled;
2040 features.blockListRequested = false;
2041 synchronized (this.disco) {
2042 this.disco.clear();
2043 }
2044 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
2045 mPendingServiceDiscoveries.set(0);
2046 mWaitForDisco.set(waitForDisco);
2047 lastDiscoStarted = SystemClock.elapsedRealtime();
2048 mXmppConnectionService.scheduleWakeUpCall(
2049 Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
2050 final Element caps = streamFeatures.findChild("c");
2051 final String hash = caps == null ? null : caps.getAttribute("hash");
2052 final String ver = caps == null ? null : caps.getAttribute("ver");
2053 ServiceDiscoveryResult discoveryResult = null;
2054 if (hash != null && ver != null) {
2055 discoveryResult =
2056 mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
2057 }
2058 final boolean requestDiscoItemsFirst =
2059 !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
2060 if (requestDiscoItemsFirst) {
2061 sendServiceDiscoveryItems(account.getDomain());
2062 }
2063 if (discoveryResult == null) {
2064 sendServiceDiscoveryInfo(account.getDomain());
2065 } else {
2066 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
2067 disco.put(account.getDomain(), discoveryResult);
2068 }
2069 discoverMamPreferences();
2070 sendServiceDiscoveryInfo(account.getJid().asBareJid());
2071 if (!requestDiscoItemsFirst) {
2072 sendServiceDiscoveryItems(account.getDomain());
2073 }
2074
2075 if (!mWaitForDisco.get()) {
2076 finalizeBind();
2077 }
2078 this.lastSessionStarted = SystemClock.elapsedRealtime();
2079 }
2080
2081 private void sendServiceDiscoveryInfo(final Jid jid) {
2082 mPendingServiceDiscoveries.incrementAndGet();
2083 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2084 iq.setTo(jid);
2085 iq.query("http://jabber.org/protocol/disco#info");
2086 this.sendIqPacket(
2087 iq,
2088 (account, packet) -> {
2089 if (packet.getType() == IqPacket.TYPE.RESULT) {
2090 boolean advancedStreamFeaturesLoaded;
2091 synchronized (XmppConnection.this.disco) {
2092 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
2093 if (jid.equals(account.getDomain())) {
2094 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
2095 result);
2096 }
2097 disco.put(jid, result);
2098 advancedStreamFeaturesLoaded =
2099 disco.containsKey(account.getDomain())
2100 && disco.containsKey(account.getJid().asBareJid());
2101 }
2102 if (advancedStreamFeaturesLoaded
2103 && (jid.equals(account.getDomain())
2104 || jid.equals(account.getJid().asBareJid()))) {
2105 enableAdvancedStreamFeatures();
2106 }
2107 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
2108 Log.d(
2109 Config.LOGTAG,
2110 account.getJid().asBareJid()
2111 + ": could not query disco info for "
2112 + jid.toString());
2113 final boolean serverOrAccount =
2114 jid.equals(account.getDomain())
2115 || jid.equals(account.getJid().asBareJid());
2116 final boolean advancedStreamFeaturesLoaded;
2117 if (serverOrAccount) {
2118 synchronized (XmppConnection.this.disco) {
2119 disco.put(jid, ServiceDiscoveryResult.empty());
2120 advancedStreamFeaturesLoaded =
2121 disco.containsKey(account.getDomain())
2122 && disco.containsKey(account.getJid().asBareJid());
2123 }
2124 } else {
2125 advancedStreamFeaturesLoaded = false;
2126 }
2127 if (advancedStreamFeaturesLoaded) {
2128 enableAdvancedStreamFeatures();
2129 }
2130 }
2131 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2132 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2133 && mWaitForDisco.compareAndSet(true, false)) {
2134 finalizeBind();
2135 }
2136 }
2137 });
2138 }
2139
2140 private void discoverMamPreferences() {
2141 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2142 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
2143 sendIqPacket(
2144 request,
2145 (account, response) -> {
2146 if (response.getType() == IqPacket.TYPE.RESULT) {
2147 Element prefs =
2148 response.findChild(
2149 "prefs", MessageArchiveService.Version.MAM_2.namespace);
2150 isMamPreferenceAlways =
2151 "always"
2152 .equals(
2153 prefs == null
2154 ? null
2155 : prefs.getAttribute("default"));
2156 }
2157 });
2158 }
2159
2160 private void discoverCommands() {
2161 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2162 request.setTo(account.getDomain());
2163 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
2164 sendIqPacket(
2165 request,
2166 (account, response) -> {
2167 if (response.getType() == IqPacket.TYPE.RESULT) {
2168 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
2169 if (query == null) {
2170 return;
2171 }
2172 final HashMap<String, Jid> commands = new HashMap<>();
2173 for (final Element child : query.getChildren()) {
2174 if ("item".equals(child.getName())) {
2175 final String node = child.getAttribute("node");
2176 final Jid jid = child.getAttributeAsJid("jid");
2177 if (node != null && jid != null) {
2178 commands.put(node, jid);
2179 }
2180 }
2181 }
2182 synchronized (this.commands) {
2183 this.commands.clear();
2184 this.commands.putAll(commands);
2185 }
2186 }
2187 });
2188 }
2189
2190 public boolean isMamPreferenceAlways() {
2191 return isMamPreferenceAlways;
2192 }
2193
2194 private void finalizeBind() {
2195 if (bindListener != null) {
2196 bindListener.onBind(account);
2197 }
2198 changeStatusToOnline();
2199 }
2200
2201 private void enableAdvancedStreamFeatures() {
2202 if (getFeatures().blocking() && !features.blockListRequested) {
2203 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
2204 this.sendIqPacket(
2205 getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
2206 }
2207 for (final OnAdvancedStreamFeaturesLoaded listener :
2208 advancedStreamFeaturesLoadedListeners) {
2209 listener.onAdvancedStreamFeaturesAvailable(account);
2210 }
2211 if (getFeatures().carbons() && !features.carbonsEnabled) {
2212 sendEnableCarbons();
2213 }
2214 if (getFeatures().commands()) {
2215 discoverCommands();
2216 }
2217 }
2218
2219 private void sendServiceDiscoveryItems(final Jid server) {
2220 mPendingServiceDiscoveries.incrementAndGet();
2221 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2222 iq.setTo(server.getDomain());
2223 iq.query("http://jabber.org/protocol/disco#items");
2224 this.sendIqPacket(
2225 iq,
2226 (account, packet) -> {
2227 if (packet.getType() == IqPacket.TYPE.RESULT) {
2228 final HashSet<Jid> items = new HashSet<>();
2229 final List<Element> elements = packet.query().getChildren();
2230 for (final Element element : elements) {
2231 if (element.getName().equals("item")) {
2232 final Jid jid =
2233 InvalidJid.getNullForInvalid(
2234 element.getAttributeAsJid("jid"));
2235 if (jid != null && !jid.equals(account.getDomain())) {
2236 items.add(jid);
2237 }
2238 }
2239 }
2240 for (Jid jid : items) {
2241 sendServiceDiscoveryInfo(jid);
2242 }
2243 } else {
2244 Log.d(
2245 Config.LOGTAG,
2246 account.getJid().asBareJid()
2247 + ": could not query disco items of "
2248 + server);
2249 }
2250 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
2251 if (mPendingServiceDiscoveries.decrementAndGet() == 0
2252 && mWaitForDisco.compareAndSet(true, false)) {
2253 finalizeBind();
2254 }
2255 }
2256 });
2257 }
2258
2259 private void sendEnableCarbons() {
2260 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2261 iq.addChild("enable", Namespace.CARBONS);
2262 this.sendIqPacket(
2263 iq,
2264 (account, packet) -> {
2265 if (packet.getType() == IqPacket.TYPE.RESULT) {
2266 Log.d(
2267 Config.LOGTAG,
2268 account.getJid().asBareJid() + ": successfully enabled carbons");
2269 features.carbonsEnabled = true;
2270 } else {
2271 Log.d(
2272 Config.LOGTAG,
2273 account.getJid().asBareJid()
2274 + ": could not enable carbons "
2275 + packet);
2276 }
2277 });
2278 }
2279
2280 private void processStreamError(final Tag currentTag) throws IOException {
2281 final Element streamError = tagReader.readElement(currentTag);
2282 if (streamError == null) {
2283 return;
2284 }
2285 if (streamError.hasChild("conflict")) {
2286 account.setResource(createNewResource());
2287 Log.d(
2288 Config.LOGTAG,
2289 account.getJid().asBareJid()
2290 + ": switching resource due to conflict ("
2291 + account.getResource()
2292 + ")");
2293 throw new IOException();
2294 } else if (streamError.hasChild("host-unknown")) {
2295 throw new StateChangingException(Account.State.HOST_UNKNOWN);
2296 } else if (streamError.hasChild("policy-violation")) {
2297 this.lastConnect = SystemClock.elapsedRealtime();
2298 final String text = streamError.findChildContent("text");
2299 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
2300 failPendingMessages(text);
2301 throw new StateChangingException(Account.State.POLICY_VIOLATION);
2302 } else if (streamError.hasChild("see-other-host")) {
2303 final String seeOtherHost = streamError.findChildContent("see-other-host");
2304 final Resolver.Result currentResolverResult = this.currentResolverResult;
2305 if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
2306 Log.d(
2307 Config.LOGTAG,
2308 account.getJid().asBareJid() + ": stream error " + streamError);
2309 throw new StateChangingException(Account.State.STREAM_ERROR);
2310 }
2311 Log.d(
2312 Config.LOGTAG,
2313 account.getJid().asBareJid()
2314 + ": see other host: "
2315 + seeOtherHost
2316 + " "
2317 + currentResolverResult);
2318 final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
2319 if (seeOtherResult != null) {
2320 this.seeOtherHostResolverResult = seeOtherResult;
2321 throw new StateChangingException(Account.State.SEE_OTHER_HOST);
2322 } else {
2323 throw new StateChangingException(Account.State.STREAM_ERROR);
2324 }
2325 } else {
2326 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
2327 throw new StateChangingException(Account.State.STREAM_ERROR);
2328 }
2329 }
2330
2331 private void failPendingMessages(final String error) {
2332 synchronized (this.mStanzaQueue) {
2333 for (int i = 0; i < mStanzaQueue.size(); ++i) {
2334 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
2335 if (stanza instanceof MessagePacket) {
2336 final MessagePacket packet = (MessagePacket) stanza;
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) {
2462 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2463
2464 if (this.mStanzaQueue.size() != 0) {
2465 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2466 if (currentHighestKey != stanzasSent) {
2467 throw new AssertionError("Stanza count messed up");
2468 }
2469 }
2470
2471 ++stanzasSent;
2472 if (Config.EXTENDED_SM_LOGGING) {
2473 Log.d(
2474 Config.LOGTAG,
2475 account.getJid().asBareJid()
2476 + ": counting outbound "
2477 + packet.getName()
2478 + " as #"
2479 + stanzasSent);
2480 }
2481 this.mStanzaQueue.append(stanzasSent, stanza);
2482 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2483 if (Config.EXTENDED_SM_LOGGING) {
2484 Log.d(
2485 Config.LOGTAG,
2486 account.getJid().asBareJid()
2487 + ": requesting ack for message stanza #"
2488 + stanzasSent);
2489 }
2490 tagWriter.writeStanzaAsync(new RequestPacket());
2491 }
2492 }
2493 }
2494 }
2495
2496 public void sendPing() {
2497 if (!r()) {
2498 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2499 iq.setFrom(account.getJid());
2500 iq.addChild("ping", Namespace.PING);
2501 this.sendIqPacket(iq, null);
2502 }
2503 this.lastPingSent = SystemClock.elapsedRealtime();
2504 }
2505
2506 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2507 this.messageListener = listener;
2508 }
2509
2510 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2511 this.unregisteredIqListener = listener;
2512 }
2513
2514 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2515 this.presenceListener = listener;
2516 }
2517
2518 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2519 this.jingleListener = listener;
2520 }
2521
2522 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2523 this.statusListener = listener;
2524 }
2525
2526 public void setOnBindListener(final OnBindListener listener) {
2527 this.bindListener = listener;
2528 }
2529
2530 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2531 this.acknowledgedListener = listener;
2532 }
2533
2534 public void addOnAdvancedStreamFeaturesAvailableListener(
2535 final OnAdvancedStreamFeaturesLoaded listener) {
2536 this.advancedStreamFeaturesLoadedListeners.add(listener);
2537 }
2538
2539 private void forceCloseSocket() {
2540 FileBackend.close(this.socket);
2541 FileBackend.close(this.tagReader);
2542 }
2543
2544 public void interrupt() {
2545 if (this.mThread != null) {
2546 this.mThread.interrupt();
2547 }
2548 }
2549
2550 public void disconnect(final boolean force) {
2551 interrupt();
2552 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2553 if (force) {
2554 forceCloseSocket();
2555 } else {
2556 final TagWriter currentTagWriter = this.tagWriter;
2557 if (currentTagWriter.isActive()) {
2558 currentTagWriter.finish();
2559 final Socket currentSocket = this.socket;
2560 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2561 try {
2562 currentTagWriter.await(1, TimeUnit.SECONDS);
2563 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2564 currentTagWriter.writeTag(Tag.end("stream:stream"));
2565 if (streamCountDownLatch != null) {
2566 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2567 Log.d(
2568 Config.LOGTAG,
2569 account.getJid().asBareJid() + ": remote ended stream");
2570 } else {
2571 Log.d(
2572 Config.LOGTAG,
2573 account.getJid().asBareJid()
2574 + ": remote has not closed socket. force closing");
2575 }
2576 }
2577 } catch (InterruptedException e) {
2578 Log.d(
2579 Config.LOGTAG,
2580 account.getJid().asBareJid()
2581 + ": interrupted while gracefully closing stream");
2582 } catch (final IOException e) {
2583 Log.d(
2584 Config.LOGTAG,
2585 account.getJid().asBareJid()
2586 + ": io exception during disconnect ("
2587 + e.getMessage()
2588 + ")");
2589 } finally {
2590 FileBackend.close(currentSocket);
2591 }
2592 } else {
2593 forceCloseSocket();
2594 }
2595 }
2596 }
2597
2598 private void resetStreamId() {
2599 this.streamId = null;
2600 this.boundStreamFeatures = null;
2601 }
2602
2603 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2604 synchronized (this.disco) {
2605 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2606 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2607 if (cursor.getValue().getFeatures().contains(feature)) {
2608 items.add(cursor);
2609 }
2610 }
2611 return items;
2612 }
2613 }
2614
2615 public Jid findDiscoItemByFeature(final String feature) {
2616 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2617 if (items.size() >= 1) {
2618 return items.get(0).getKey();
2619 }
2620 return null;
2621 }
2622
2623 public boolean r() {
2624 if (getFeatures().sm()) {
2625 this.tagWriter.writeStanzaAsync(new RequestPacket());
2626 return true;
2627 } else {
2628 return false;
2629 }
2630 }
2631
2632 public List<String> getMucServersWithholdAccount() {
2633 final List<String> servers = getMucServers();
2634 servers.remove(account.getDomain().toEscapedString());
2635 return servers;
2636 }
2637
2638 public List<String> getMucServers() {
2639 List<String> servers = new ArrayList<>();
2640 synchronized (this.disco) {
2641 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2642 final ServiceDiscoveryResult value = cursor.getValue();
2643 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2644 && value.hasIdentity("conference", "text")
2645 && !value.getFeatures().contains("jabber:iq:gateway")
2646 && !value.hasIdentity("conference", "irc")) {
2647 servers.add(cursor.getKey().toString());
2648 }
2649 }
2650 }
2651 return servers;
2652 }
2653
2654 public String getMucServer() {
2655 List<String> servers = getMucServers();
2656 return servers.size() > 0 ? servers.get(0) : null;
2657 }
2658
2659 public int getTimeToNextAttempt(final boolean aggressive) {
2660 final int interval;
2661 if (aggressive) {
2662 interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
2663 } else {
2664 final int additionalTime =
2665 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2666 interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2667 }
2668 final int secondsSinceLast =
2669 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2670 return interval - secondsSinceLast;
2671 }
2672
2673 public int getAttempt() {
2674 return this.attempt;
2675 }
2676
2677 public Features getFeatures() {
2678 return this.features;
2679 }
2680
2681 public long getLastSessionEstablished() {
2682 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2683 return System.currentTimeMillis() - diff;
2684 }
2685
2686 public long getLastConnect() {
2687 return this.lastConnect;
2688 }
2689
2690 public long getLastPingSent() {
2691 return this.lastPingSent;
2692 }
2693
2694 public long getLastDiscoStarted() {
2695 return this.lastDiscoStarted;
2696 }
2697
2698 public long getLastPacketReceived() {
2699 return this.lastPacketReceived;
2700 }
2701
2702 public void sendActive() {
2703 this.sendPacket(new ActivePacket());
2704 }
2705
2706 public void sendInactive() {
2707 this.sendPacket(new InactivePacket());
2708 }
2709
2710 public void resetAttemptCount(boolean resetConnectTime) {
2711 this.attempt = 0;
2712 if (resetConnectTime) {
2713 this.lastConnect = 0;
2714 }
2715 }
2716
2717 public void setInteractive(boolean interactive) {
2718 this.mInteractive = interactive;
2719 }
2720
2721 private IqGenerator getIqGenerator() {
2722 return mXmppConnectionService.getIqGenerator();
2723 }
2724
2725 private class MyKeyManager implements X509KeyManager {
2726 @Override
2727 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2728 return account.getPrivateKeyAlias();
2729 }
2730
2731 @Override
2732 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2733 return null;
2734 }
2735
2736 @Override
2737 public X509Certificate[] getCertificateChain(String alias) {
2738 Log.d(Config.LOGTAG, "getting certificate chain");
2739 try {
2740 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2741 } catch (final Exception e) {
2742 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2743 return new X509Certificate[0];
2744 }
2745 }
2746
2747 @Override
2748 public String[] getClientAliases(String s, Principal[] principals) {
2749 final String alias = account.getPrivateKeyAlias();
2750 return alias != null ? new String[] {alias} : new String[0];
2751 }
2752
2753 @Override
2754 public String[] getServerAliases(String s, Principal[] principals) {
2755 return new String[0];
2756 }
2757
2758 @Override
2759 public PrivateKey getPrivateKey(String alias) {
2760 try {
2761 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2762 } catch (Exception e) {
2763 return null;
2764 }
2765 }
2766 }
2767
2768 private static class LoginInfo {
2769 public final SaslMechanism saslMechanism;
2770 public final SaslMechanism.Version saslVersion;
2771 public final List<String> inlineBindFeatures;
2772
2773 private LoginInfo(
2774 final SaslMechanism saslMechanism,
2775 final SaslMechanism.Version saslVersion,
2776 final Collection<String> inlineBindFeatures) {
2777 Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
2778 Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
2779 this.saslMechanism = saslMechanism;
2780 this.saslVersion = saslVersion;
2781 this.inlineBindFeatures =
2782 inlineBindFeatures == null
2783 ? Collections.emptyList()
2784 : ImmutableList.copyOf(inlineBindFeatures);
2785 }
2786
2787 public static SaslMechanism mechanism(final LoginInfo loginInfo) {
2788 return loginInfo == null ? null : loginInfo.saslMechanism;
2789 }
2790 }
2791
2792 private static class StreamId {
2793 public final String id;
2794 public final Resolver.Result location;
2795
2796 private StreamId(String id, Resolver.Result location) {
2797 this.id = id;
2798 this.location = location;
2799 }
2800
2801 @NonNull
2802 @Override
2803 public String toString() {
2804 return MoreObjects.toStringHelper(this)
2805 .add("id", id)
2806 .add("location", location)
2807 .toString();
2808 }
2809 }
2810
2811 private static class StateChangingError extends Error {
2812 private final Account.State state;
2813
2814 public StateChangingError(Account.State state) {
2815 this.state = state;
2816 }
2817 }
2818
2819 private static class StateChangingException extends IOException {
2820 private final Account.State state;
2821
2822 public StateChangingException(Account.State state) {
2823 this.state = state;
2824 }
2825 }
2826
2827 public class Features {
2828 XmppConnection connection;
2829 private boolean carbonsEnabled = false;
2830 private boolean encryptionEnabled = false;
2831 private boolean blockListRequested = false;
2832
2833 public Features(final XmppConnection connection) {
2834 this.connection = connection;
2835 }
2836
2837 private boolean hasDiscoFeature(final Jid server, final String feature) {
2838 synchronized (XmppConnection.this.disco) {
2839 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2840 return sdr != null && sdr.getFeatures().contains(feature);
2841 }
2842 }
2843
2844 public boolean carbons() {
2845 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2846 }
2847
2848 public boolean commands() {
2849 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2850 }
2851
2852 public boolean easyOnboardingInvites() {
2853 synchronized (commands) {
2854 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2855 }
2856 }
2857
2858 public boolean bookmarksConversion() {
2859 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2860 && pepPublishOptions();
2861 }
2862
2863 public boolean avatarConversion() {
2864 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2865 && pepPublishOptions();
2866 }
2867
2868 public boolean blocking() {
2869 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2870 }
2871
2872 public boolean spamReporting() {
2873 return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
2874 }
2875
2876 public boolean flexibleOfflineMessageRetrieval() {
2877 return hasDiscoFeature(
2878 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2879 }
2880
2881 public boolean register() {
2882 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2883 }
2884
2885 public boolean invite() {
2886 return connection.streamFeatures != null
2887 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2888 }
2889
2890 public boolean sm() {
2891 return streamId != null
2892 || (connection.streamFeatures != null
2893 && connection.streamFeatures.hasChild(
2894 "sm", Namespace.STREAM_MANAGEMENT));
2895 }
2896
2897 public boolean csi() {
2898 return connection.streamFeatures != null
2899 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2900 }
2901
2902 public boolean pep() {
2903 synchronized (XmppConnection.this.disco) {
2904 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2905 return info != null && info.hasIdentity("pubsub", "pep");
2906 }
2907 }
2908
2909 public boolean pepPersistent() {
2910 synchronized (XmppConnection.this.disco) {
2911 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2912 return info != null
2913 && info.getFeatures()
2914 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2915 }
2916 }
2917
2918 public boolean pepPublishOptions() {
2919 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2920 }
2921
2922 public boolean pepOmemoWhitelisted() {
2923 return hasDiscoFeature(
2924 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2925 }
2926
2927 public boolean mam() {
2928 return MessageArchiveService.Version.has(getAccountFeatures());
2929 }
2930
2931 public List<String> getAccountFeatures() {
2932 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2933 return result == null ? Collections.emptyList() : result.getFeatures();
2934 }
2935
2936 public boolean push() {
2937 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2938 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2939 }
2940
2941 public boolean rosterVersioning() {
2942 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2943 }
2944
2945 public void setBlockListRequested(boolean value) {
2946 this.blockListRequested = value;
2947 }
2948
2949 public boolean httpUpload(long filesize) {
2950 if (Config.DISABLE_HTTP_UPLOAD) {
2951 return false;
2952 } else {
2953 for (String namespace :
2954 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2955 List<Entry<Jid, ServiceDiscoveryResult>> items =
2956 findDiscoItemsByFeature(namespace);
2957 if (items.size() > 0) {
2958 try {
2959 long maxsize =
2960 Long.parseLong(
2961 items.get(0)
2962 .getValue()
2963 .getExtendedDiscoInformation(
2964 namespace, "max-file-size"));
2965 if (filesize <= maxsize) {
2966 return true;
2967 } else {
2968 Log.d(
2969 Config.LOGTAG,
2970 account.getJid().asBareJid()
2971 + ": http upload is not available for files with size "
2972 + filesize
2973 + " (max is "
2974 + maxsize
2975 + ")");
2976 return false;
2977 }
2978 } catch (Exception e) {
2979 return true;
2980 }
2981 }
2982 }
2983 return false;
2984 }
2985 }
2986
2987 public boolean useLegacyHttpUpload() {
2988 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2989 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2990 }
2991
2992 public long getMaxHttpUploadSize() {
2993 for (String namespace :
2994 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2995 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2996 if (items.size() > 0) {
2997 try {
2998 return Long.parseLong(
2999 items.get(0)
3000 .getValue()
3001 .getExtendedDiscoInformation(namespace, "max-file-size"));
3002 } catch (Exception e) {
3003 // ignored
3004 }
3005 }
3006 }
3007 return -1;
3008 }
3009
3010 public boolean stanzaIds() {
3011 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
3012 }
3013
3014 public boolean bookmarks2() {
3015 return pepPublishOptions()
3016 && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
3017 }
3018
3019 public boolean externalServiceDiscovery() {
3020 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
3021 }
3022 }
3023}