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