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 //TODO store mechanism name
722 account.setKey(Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority()));
723 if (version == SaslMechanism.Version.SASL_2) {
724 final String authorizationIdentifier =
725 success.findChildContent("authorization-identifier");
726 final Jid authorizationJid;
727 try {
728 authorizationJid =
729 Strings.isNullOrEmpty(authorizationIdentifier)
730 ? null
731 : Jid.ofEscaped(authorizationIdentifier);
732 } catch (final IllegalArgumentException e) {
733 Log.d(
734 Config.LOGTAG,
735 account.getJid().asBareJid()
736 + ": SASL 2.0 authorization identifier was not a valid jid");
737 throw new StateChangingException(Account.State.BIND_FAILURE);
738 }
739 if (authorizationJid == null) {
740 throw new StateChangingException(Account.State.BIND_FAILURE);
741 }
742 Log.d(
743 Config.LOGTAG,
744 account.getJid().asBareJid()
745 + ": SASL 2.0 authorization identifier was "
746 + authorizationJid);
747 if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) {
748 Log.d(
749 Config.LOGTAG,
750 account.getJid().asBareJid()
751 + ": server tried to re-assign domain to "
752 + authorizationJid.getDomain());
753 throw new StateChangingError(Account.State.BIND_FAILURE);
754 }
755 if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
756 Log.d(
757 Config.LOGTAG,
758 account.getJid().asBareJid()
759 + ": jid changed during SASL 2.0. updating database");
760 mXmppConnectionService.databaseBackend.updateAccount(account);
761 }
762 final Element bound = success.findChild("bound", Namespace.BIND2);
763 final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3");
764 final Element failed = success.findChild("failed", "urn:xmpp:sm:3");
765 // TODO check if resumed and bound exist and throw bind failure
766 if (resumed != null && streamId != null) {
767 processResumed(resumed);
768 } else if (failed != null) {
769 processFailed(failed, false); // wait for new stream features
770 }
771 if (bound != null) {
772 this.isBound = true;
773 final Element streamManagementEnabled =
774 bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
775 final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
776 if (streamManagementEnabled != null) {
777 processEnabled(streamManagementEnabled);
778 }
779 if (carbonsEnabled != null) {
780 Log.d(
781 Config.LOGTAG,
782 account.getJid().asBareJid() + ": successfully enabled carbons");
783 features.carbonsEnabled = true;
784 }
785 //TODO if both are set mark account ready for pipelining
786 sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null);
787 }
788 }
789 if (version == SaslMechanism.Version.SASL) {
790 tagReader.reset();
791 sendStartStream();
792 final Tag tag = tagReader.readTag();
793 if (tag != null && tag.isStart("stream")) {
794 processStream();
795 return true;
796 } else {
797 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
798 }
799 } else {
800 return false;
801 }
802 }
803
804 private void processEnabled(final Element enabled) {
805 final String streamId;
806 if (enabled.getAttributeAsBoolean("resume")) {
807 streamId = enabled.getAttribute("id");
808 Log.d(
809 Config.LOGTAG,
810 account.getJid().asBareJid().toString()
811 + ": stream management enabled (resumable)");
812 } else {
813 Log.d(
814 Config.LOGTAG,
815 account.getJid().asBareJid().toString() + ": stream management enabled");
816 streamId = null;
817 }
818 this.streamId = streamId;
819 this.stanzasReceived = 0;
820 this.inSmacksSession = true;
821 final RequestPacket r = new RequestPacket();
822 tagWriter.writeStanzaAsync(r);
823 }
824
825 private void processResumed(final Element resumed) throws StateChangingException {
826 this.inSmacksSession = true;
827 this.isBound = true;
828 this.tagWriter.writeStanzaAsync(new RequestPacket());
829 lastPacketReceived = SystemClock.elapsedRealtime();
830 final String h = resumed.getAttribute("h");
831 if (h == null) {
832 resetStreamId();
833 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
834 }
835 final int serverCount;
836 try {
837 serverCount = Integer.parseInt(h);
838 } catch (final NumberFormatException e) {
839 resetStreamId();
840 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
841 }
842 final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
843 final boolean acknowledgedMessages;
844 synchronized (this.mStanzaQueue) {
845 if (serverCount < stanzasSent) {
846 Log.d(
847 Config.LOGTAG,
848 account.getJid().asBareJid() + ": session resumed with lost packages");
849 stanzasSent = serverCount;
850 } else {
851 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": session resumed");
852 }
853 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
854 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
855 failedStanzas.add(mStanzaQueue.valueAt(i));
856 }
857 mStanzaQueue.clear();
858 }
859 if (acknowledgedMessages) {
860 mXmppConnectionService.updateConversationUi();
861 }
862 Log.d(
863 Config.LOGTAG,
864 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
865 for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
866 if (packet instanceof MessagePacket) {
867 MessagePacket message = (MessagePacket) packet;
868 mXmppConnectionService.markMessage(
869 account,
870 message.getTo().asBareJid(),
871 message.getId(),
872 Message.STATUS_UNSEND);
873 }
874 sendPacket(packet);
875 }
876 Log.d(
877 Config.LOGTAG,
878 account.getJid().asBareJid() + ": online with resource " + account.getResource());
879 changeStatus(Account.State.ONLINE);
880 }
881
882 private void processFailed(final Element failed, final boolean sendBindRequest) {
883 final int serverCount;
884 try {
885 serverCount = Integer.parseInt(failed.getAttribute("h"));
886 } catch (final NumberFormatException | NullPointerException e) {
887 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed");
888 resetStreamId();
889 if (sendBindRequest) {
890 sendBindRequest();
891 }
892 return;
893 }
894 Log.d(
895 Config.LOGTAG,
896 account.getJid().asBareJid()
897 + ": resumption failed but server acknowledged stanza #"
898 + serverCount);
899 final boolean acknowledgedMessages;
900 synchronized (this.mStanzaQueue) {
901 acknowledgedMessages = acknowledgeStanzaUpTo(serverCount);
902 }
903 if (acknowledgedMessages) {
904 mXmppConnectionService.updateConversationUi();
905 }
906 resetStreamId();
907 if (sendBindRequest) {
908 sendBindRequest();
909 }
910 }
911
912 private boolean acknowledgeStanzaUpTo(int serverCount) {
913 if (serverCount > stanzasSent) {
914 Log.e(
915 Config.LOGTAG,
916 "server acknowledged more stanzas than we sent. serverCount="
917 + serverCount
918 + ", ourCount="
919 + stanzasSent);
920 }
921 boolean acknowledgedMessages = false;
922 for (int i = 0; i < mStanzaQueue.size(); ++i) {
923 if (serverCount >= mStanzaQueue.keyAt(i)) {
924 if (Config.EXTENDED_SM_LOGGING) {
925 Log.d(
926 Config.LOGTAG,
927 account.getJid().asBareJid()
928 + ": server acknowledged stanza #"
929 + mStanzaQueue.keyAt(i));
930 }
931 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
932 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
933 final MessagePacket packet = (MessagePacket) stanza;
934 final String id = packet.getId();
935 final Jid to = packet.getTo();
936 if (id != null && to != null) {
937 acknowledgedMessages |=
938 acknowledgedListener.onMessageAcknowledged(account, to, id);
939 }
940 }
941 mStanzaQueue.removeAt(i);
942 i--;
943 }
944 }
945 return acknowledgedMessages;
946 }
947
948 private @NonNull Element processPacket(final Tag currentTag, final int packetType)
949 throws IOException {
950 final Element element;
951 switch (packetType) {
952 case PACKET_IQ:
953 element = new IqPacket();
954 break;
955 case PACKET_MESSAGE:
956 element = new MessagePacket();
957 break;
958 case PACKET_PRESENCE:
959 element = new PresencePacket();
960 break;
961 default:
962 throw new AssertionError("Should never encounter invalid type");
963 }
964 element.setAttributes(currentTag.getAttributes());
965 Tag nextTag = tagReader.readTag();
966 if (nextTag == null) {
967 throw new IOException("interrupted mid tag");
968 }
969 while (!nextTag.isEnd(element.getName())) {
970 if (!nextTag.isNo()) {
971 element.addChild(tagReader.readElement(nextTag));
972 }
973 nextTag = tagReader.readTag();
974 if (nextTag == null) {
975 throw new IOException("interrupted mid tag");
976 }
977 }
978 if (stanzasReceived == Integer.MAX_VALUE) {
979 resetStreamId();
980 throw new IOException("time to restart the session. cant handle >2 billion pcks");
981 }
982 if (inSmacksSession) {
983 ++stanzasReceived;
984 } else if (features.sm()) {
985 Log.d(
986 Config.LOGTAG,
987 account.getJid().asBareJid()
988 + ": not counting stanza("
989 + element.getClass().getSimpleName()
990 + "). Not in smacks session.");
991 }
992 lastPacketReceived = SystemClock.elapsedRealtime();
993 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
994 Log.d(Config.LOGTAG, "[background stanza] " + element);
995 }
996 if (element instanceof IqPacket
997 && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
998 && element.hasChild("jingle", Namespace.JINGLE)) {
999 return JinglePacket.upgrade((IqPacket) element);
1000 } else {
1001 return element;
1002 }
1003 }
1004
1005 private void processIq(final Tag currentTag) throws IOException {
1006 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
1007 if (!packet.valid()) {
1008 Log.e(
1009 Config.LOGTAG,
1010 "encountered invalid iq from='"
1011 + packet.getFrom()
1012 + "' to='"
1013 + packet.getTo()
1014 + "'");
1015 return;
1016 }
1017 if (packet instanceof JinglePacket) {
1018 if (this.jingleListener != null) {
1019 this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
1020 }
1021 } else {
1022 OnIqPacketReceived callback = null;
1023 synchronized (this.packetCallbacks) {
1024 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple =
1025 packetCallbacks.get(packet.getId());
1026 if (packetCallbackDuple != null) {
1027 // Packets to the server should have responses from the server
1028 if (packetCallbackDuple.first.toServer(account)) {
1029 if (packet.fromServer(account)) {
1030 callback = packetCallbackDuple.second;
1031 packetCallbacks.remove(packet.getId());
1032 } else {
1033 Log.e(
1034 Config.LOGTAG,
1035 account.getJid().asBareJid().toString()
1036 + ": ignoring spoofed iq packet");
1037 }
1038 } else {
1039 if (packet.getFrom() != null
1040 && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
1041 callback = packetCallbackDuple.second;
1042 packetCallbacks.remove(packet.getId());
1043 } else {
1044 Log.e(
1045 Config.LOGTAG,
1046 account.getJid().asBareJid().toString()
1047 + ": ignoring spoofed iq packet");
1048 }
1049 }
1050 } else if (packet.getType() == IqPacket.TYPE.GET
1051 || packet.getType() == IqPacket.TYPE.SET) {
1052 callback = this.unregisteredIqListener;
1053 }
1054 }
1055 if (callback != null) {
1056 try {
1057 callback.onIqPacketReceived(account, packet);
1058 } catch (StateChangingError error) {
1059 throw new StateChangingException(error.state);
1060 }
1061 }
1062 }
1063 }
1064
1065 private void processMessage(final Tag currentTag) throws IOException {
1066 final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
1067 if (!packet.valid()) {
1068 Log.e(
1069 Config.LOGTAG,
1070 "encountered invalid message from='"
1071 + packet.getFrom()
1072 + "' to='"
1073 + packet.getTo()
1074 + "'");
1075 return;
1076 }
1077 this.messageListener.onMessagePacketReceived(account, packet);
1078 }
1079
1080 private void processPresence(final Tag currentTag) throws IOException {
1081 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
1082 if (!packet.valid()) {
1083 Log.e(
1084 Config.LOGTAG,
1085 "encountered invalid presence from='"
1086 + packet.getFrom()
1087 + "' to='"
1088 + packet.getTo()
1089 + "'");
1090 return;
1091 }
1092 this.presenceListener.onPresencePacketReceived(account, packet);
1093 }
1094
1095 private void sendStartTLS() throws IOException {
1096 final Tag startTLS = Tag.empty("starttls");
1097 startTLS.setAttribute("xmlns", Namespace.TLS);
1098 tagWriter.writeTag(startTLS);
1099 }
1100
1101 private void switchOverToTls() throws XmlPullParserException, IOException {
1102 tagReader.readTag();
1103 final Socket socket = this.socket;
1104 final SSLSocket sslSocket = upgradeSocketToTls(socket);
1105 tagReader.setInputStream(sslSocket.getInputStream());
1106 tagWriter.setOutputStream(sslSocket.getOutputStream());
1107 sendStartStream();
1108 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
1109 features.encryptionEnabled = true;
1110 final Tag tag = tagReader.readTag();
1111 if (tag != null && tag.isStart("stream")) {
1112 SSLSocketHelper.log(account, sslSocket);
1113 processStream();
1114 } else {
1115 throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
1116 }
1117 sslSocket.close();
1118 }
1119
1120 private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
1121 final SSLSocketFactory sslSocketFactory;
1122 try {
1123 sslSocketFactory = getSSLSocketFactory();
1124 } catch (final NoSuchAlgorithmException | KeyManagementException e) {
1125 throw new StateChangingException(Account.State.TLS_ERROR);
1126 }
1127 final InetAddress address = socket.getInetAddress();
1128 final SSLSocket sslSocket =
1129 (SSLSocket)
1130 sslSocketFactory.createSocket(
1131 socket, address.getHostAddress(), socket.getPort(), true);
1132 SSLSocketHelper.setSecurity(sslSocket);
1133 SSLSocketHelper.setHostname(sslSocket, IDN.toASCII(account.getServer()));
1134 SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
1135 final XmppDomainVerifier xmppDomainVerifier = new XmppDomainVerifier();
1136 try {
1137 if (!xmppDomainVerifier.verify(
1138 account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
1139 Log.d(
1140 Config.LOGTAG,
1141 account.getJid().asBareJid()
1142 + ": TLS certificate domain verification failed");
1143 FileBackend.close(sslSocket);
1144 throw new StateChangingException(Account.State.TLS_ERROR_DOMAIN);
1145 }
1146 } catch (final SSLPeerUnverifiedException e) {
1147 FileBackend.close(sslSocket);
1148 throw new StateChangingException(Account.State.TLS_ERROR);
1149 }
1150 return sslSocket;
1151 }
1152
1153 private void processStreamFeatures(final Tag currentTag) throws IOException {
1154 this.streamFeatures = tagReader.readElement(currentTag);
1155 final boolean isSecure =
1156 features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
1157 final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
1158 if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
1159 && !features.encryptionEnabled) {
1160 sendStartTLS();
1161 } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1162 && account.isOptionSet(Account.OPTION_REGISTER)) {
1163 if (isSecure) {
1164 register();
1165 } else {
1166 Log.d(
1167 Config.LOGTAG,
1168 account.getJid().asBareJid()
1169 + ": unable to find STARTTLS for registration process "
1170 + XmlHelper.printElementNames(this.streamFeatures));
1171 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1172 }
1173 } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
1174 && account.isOptionSet(Account.OPTION_REGISTER)) {
1175 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
1176 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL_2)
1177 && shouldAuthenticate
1178 && isSecure) {
1179 authenticate(SaslMechanism.Version.SASL_2);
1180 } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
1181 && shouldAuthenticate
1182 && isSecure) {
1183 authenticate(SaslMechanism.Version.SASL);
1184 } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
1185 && streamId != null
1186 && !inSmacksSession) {
1187 if (Config.EXTENDED_SM_LOGGING) {
1188 Log.d(
1189 Config.LOGTAG,
1190 account.getJid().asBareJid()
1191 + ": resuming after stanza #"
1192 + stanzasReceived);
1193 }
1194 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1195 this.mSmCatchupMessageCounter.set(0);
1196 this.mWaitingForSmCatchup.set(true);
1197 this.tagWriter.writeStanzaAsync(resume);
1198 } else if (needsBinding) {
1199 if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
1200 sendBindRequest();
1201 } else {
1202 Log.d(
1203 Config.LOGTAG,
1204 account.getJid().asBareJid()
1205 + ": unable to find bind feature "
1206 + XmlHelper.printElementNames(this.streamFeatures));
1207 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1208 }
1209 } else {
1210 Log.d(
1211 Config.LOGTAG,
1212 account.getJid().asBareJid()
1213 + ": received NOP stream features "
1214 + XmlHelper.printElementNames(this.streamFeatures));
1215 }
1216 }
1217
1218 private void authenticate(final SaslMechanism.Version version) throws IOException {
1219 final List<String> mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms"));
1220 if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
1221 saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
1222 } else if (mechanisms.contains(ScramSha512.MECHANISM)) {
1223 saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG());
1224 } else if (mechanisms.contains(ScramSha256.MECHANISM)) {
1225 saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
1226 } else if (mechanisms.contains(ScramSha1.MECHANISM)) {
1227 saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
1228 } else if (mechanisms.contains(Plain.MECHANISM)
1229 && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
1230 saslMechanism = new Plain(tagWriter, account);
1231 } else if (mechanisms.contains(DigestMd5.MECHANISM)) {
1232 saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
1233 } else if (mechanisms.contains(Anonymous.MECHANISM)) {
1234 saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
1235 }
1236 if (saslMechanism == null) {
1237 Log.d(
1238 Config.LOGTAG,
1239 account.getJid().asBareJid()
1240 + ": unable to find supported SASL mechanism in "
1241 + mechanisms);
1242 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
1243 }
1244 final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1);
1245 if (pinnedMechanism > saslMechanism.getPriority()) {
1246 Log.e(
1247 Config.LOGTAG,
1248 "Auth failed. Authentication mechanism "
1249 + saslMechanism.getMechanism()
1250 + " has lower priority ("
1251 + saslMechanism.getPriority()
1252 + ") than pinned priority ("
1253 + pinnedMechanism
1254 + "). Possible downgrade attack?");
1255 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
1256 }
1257 final String firstMessage = saslMechanism.getClientFirstMessage();
1258 final Element authenticate;
1259 if (version == SaslMechanism.Version.SASL) {
1260 authenticate = new Element("auth", Namespace.SASL);
1261 if (!Strings.isNullOrEmpty(firstMessage)) {
1262 authenticate.setContent(firstMessage);
1263 }
1264 } else if (version == SaslMechanism.Version.SASL_2) {
1265 authenticate = new Element("authenticate", Namespace.SASL_2);
1266 if (!Strings.isNullOrEmpty(firstMessage)) {
1267 authenticate.addChild("initial-response").setContent(firstMessage);
1268 }
1269 final Element inline = this.streamFeatures.findChild("inline", Namespace.SASL_2);
1270 final boolean inlineStreamManagement =
1271 inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
1272 final boolean inlineBind2 = inline != null && inline.hasChild("bind", Namespace.BIND2);
1273 final Element inlineBindFeatures =
1274 this.streamFeatures.findChild("inline", Namespace.BIND2);
1275 if (inlineBind2 && inlineBindFeatures != null) {
1276 final Element bind =
1277 generateBindRequest(
1278 Collections2.transform(
1279 inlineBindFeatures.getChildren(),
1280 c -> c == null ? null : c.getAttribute("var")));
1281 authenticate.addChild(bind);
1282 }
1283 if (inlineStreamManagement && streamId != null) {
1284 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
1285 this.mSmCatchupMessageCounter.set(0);
1286 this.mWaitingForSmCatchup.set(true);
1287 authenticate.addChild(resume);
1288 }
1289 } else {
1290 throw new AssertionError("Missing implementation for " + version);
1291 }
1292
1293 Log.d(
1294 Config.LOGTAG,
1295 account.getJid().toString()
1296 + ": Authenticating with "
1297 + version
1298 + "/"
1299 + saslMechanism.getMechanism());
1300 authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
1301 tagWriter.writeElement(authenticate);
1302 }
1303
1304 private Element generateBindRequest(final Collection<String> bindFeatures) {
1305 Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
1306 final Element bind = new Element("bind", Namespace.BIND2);
1307 final Element clientId = bind.addChild("client-id");
1308 clientId.setAttribute("tag", mXmppConnectionService.getString(R.string.app_name));
1309 clientId.setContent(account.getUuid());
1310 final Element features = bind.addChild("features");
1311 if (bindFeatures.contains(Namespace.CARBONS)) {
1312 features.addChild("enable", Namespace.CARBONS);
1313 }
1314 if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
1315 features.addChild(new EnablePacket());
1316 }
1317 return bind;
1318 }
1319
1320 private static List<String> extractMechanisms(final Element stream) {
1321 final ArrayList<String> mechanisms = new ArrayList<>(stream.getChildren().size());
1322 for (final Element child : stream.getChildren()) {
1323 mechanisms.add(child.getContent());
1324 }
1325 return mechanisms;
1326 }
1327
1328 private void register() {
1329 final String preAuth = account.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN);
1330 if (preAuth != null && features.invite()) {
1331 final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
1332 preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
1333 sendUnmodifiedIqPacket(
1334 preAuthRequest,
1335 (account, response) -> {
1336 if (response.getType() == IqPacket.TYPE.RESULT) {
1337 sendRegistryRequest();
1338 } else {
1339 final String error = response.getErrorCondition();
1340 Log.d(
1341 Config.LOGTAG,
1342 account.getJid().asBareJid()
1343 + ": failed to pre auth. "
1344 + error);
1345 throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
1346 }
1347 },
1348 true);
1349 } else {
1350 sendRegistryRequest();
1351 }
1352 }
1353
1354 private void sendRegistryRequest() {
1355 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
1356 register.query(Namespace.REGISTER);
1357 register.setTo(account.getDomain());
1358 sendUnmodifiedIqPacket(
1359 register,
1360 (account, packet) -> {
1361 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1362 return;
1363 }
1364 if (packet.getType() == IqPacket.TYPE.ERROR) {
1365 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1366 }
1367 final Element query = packet.query(Namespace.REGISTER);
1368 if (query.hasChild("username") && (query.hasChild("password"))) {
1369 final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
1370 final Element username =
1371 new Element("username").setContent(account.getUsername());
1372 final Element password =
1373 new Element("password").setContent(account.getPassword());
1374 register1.query(Namespace.REGISTER).addChild(username);
1375 register1.query().addChild(password);
1376 register1.setFrom(account.getJid().asBareJid());
1377 sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
1378 } else if (query.hasChild("x", Namespace.DATA)) {
1379 final Data data = Data.parse(query.findChild("x", Namespace.DATA));
1380 final Element blob = query.findChild("data", "urn:xmpp:bob");
1381 final String id = packet.getId();
1382 InputStream is;
1383 if (blob != null) {
1384 try {
1385 final String base64Blob = blob.getContent();
1386 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
1387 is = new ByteArrayInputStream(strBlob);
1388 } catch (Exception e) {
1389 is = null;
1390 }
1391 } else {
1392 final boolean useTor =
1393 mXmppConnectionService.useTorToConnect() || account.isOnion();
1394 try {
1395 final String url = data.getValue("url");
1396 final String fallbackUrl = data.getValue("captcha-fallback-url");
1397 if (url != null) {
1398 is = HttpConnectionManager.open(url, useTor);
1399 } else if (fallbackUrl != null) {
1400 is = HttpConnectionManager.open(fallbackUrl, useTor);
1401 } else {
1402 is = null;
1403 }
1404 } catch (final IOException e) {
1405 Log.d(
1406 Config.LOGTAG,
1407 account.getJid().asBareJid() + ": unable to fetch captcha",
1408 e);
1409 is = null;
1410 }
1411 }
1412
1413 if (is != null) {
1414 Bitmap captcha = BitmapFactory.decodeStream(is);
1415 try {
1416 if (mXmppConnectionService.displayCaptchaRequest(
1417 account, id, data, captcha)) {
1418 return;
1419 }
1420 } catch (Exception e) {
1421 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1422 }
1423 }
1424 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1425 } else if (query.hasChild("instructions")
1426 || query.hasChild("x", Namespace.OOB)) {
1427 final String instructions = query.findChildContent("instructions");
1428 final Element oob = query.findChild("x", Namespace.OOB);
1429 final String url = oob == null ? null : oob.findChildContent("url");
1430 if (url != null) {
1431 setAccountCreationFailed(url);
1432 } else if (instructions != null) {
1433 final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
1434 if (matcher.find()) {
1435 setAccountCreationFailed(
1436 instructions.substring(matcher.start(), matcher.end()));
1437 }
1438 }
1439 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1440 }
1441 },
1442 true);
1443 }
1444
1445 private void setAccountCreationFailed(final String url) {
1446 final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
1447 if (httpUrl != null && httpUrl.isHttps()) {
1448 this.redirectionUrl = httpUrl;
1449 throw new StateChangingError(Account.State.REGISTRATION_WEB);
1450 }
1451 throw new StateChangingError(Account.State.REGISTRATION_FAILED);
1452 }
1453
1454 public HttpUrl getRedirectionUrl() {
1455 return this.redirectionUrl;
1456 }
1457
1458 public void resetEverything() {
1459 resetAttemptCount(true);
1460 resetStreamId();
1461 clearIqCallbacks();
1462 this.stanzasSent = 0;
1463 mStanzaQueue.clear();
1464 this.redirectionUrl = null;
1465 synchronized (this.disco) {
1466 disco.clear();
1467 }
1468 synchronized (this.commands) {
1469 this.commands.clear();
1470 }
1471 }
1472
1473 private void sendBindRequest() {
1474 try {
1475 mXmppConnectionService.restoredFromDatabaseLatch.await();
1476 } catch (InterruptedException e) {
1477 Log.d(
1478 Config.LOGTAG,
1479 account.getJid().asBareJid()
1480 + ": interrupted while waiting for DB restore during bind");
1481 return;
1482 }
1483 clearIqCallbacks();
1484 if (account.getJid().isBareJid()) {
1485 account.setResource(this.createNewResource());
1486 } else {
1487 fixResource(mXmppConnectionService, account);
1488 }
1489 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1490 final String resource =
1491 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
1492 iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
1493 this.sendUnmodifiedIqPacket(
1494 iq,
1495 (account, packet) -> {
1496 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1497 return;
1498 }
1499 final Element bind = packet.findChild("bind");
1500 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1501 isBound = true;
1502 final Element jid = bind.findChild("jid");
1503 if (jid != null && jid.getContent() != null) {
1504 try {
1505 Jid assignedJid = Jid.ofEscaped(jid.getContent());
1506 if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
1507 Log.d(
1508 Config.LOGTAG,
1509 account.getJid().asBareJid()
1510 + ": server tried to re-assign domain to "
1511 + assignedJid.getDomain());
1512 throw new StateChangingError(Account.State.BIND_FAILURE);
1513 }
1514 if (account.setJid(assignedJid)) {
1515 Log.d(
1516 Config.LOGTAG,
1517 account.getJid().asBareJid()
1518 + ": jid changed during bind. updating database");
1519 mXmppConnectionService.databaseBackend.updateAccount(account);
1520 }
1521 if (streamFeatures.hasChild("session")
1522 && !streamFeatures
1523 .findChild("session")
1524 .hasChild("optional")) {
1525 sendStartSession();
1526 } else {
1527 final boolean waitForDisco = enableStreamManagement();
1528 sendPostBindInitialization(waitForDisco, false);
1529 }
1530 return;
1531 } catch (final IllegalArgumentException e) {
1532 Log.d(
1533 Config.LOGTAG,
1534 account.getJid().asBareJid()
1535 + ": server reported invalid jid ("
1536 + jid.getContent()
1537 + ") on bind");
1538 }
1539 } else {
1540 Log.d(
1541 Config.LOGTAG,
1542 account.getJid()
1543 + ": disconnecting because of bind failure. (no jid)");
1544 }
1545 } else {
1546 Log.d(
1547 Config.LOGTAG,
1548 account.getJid()
1549 + ": disconnecting because of bind failure ("
1550 + packet);
1551 }
1552 final Element error = packet.findChild("error");
1553 if (packet.getType() == IqPacket.TYPE.ERROR
1554 && error != null
1555 && error.hasChild("conflict")) {
1556 account.setResource(createNewResource());
1557 }
1558 throw new StateChangingError(Account.State.BIND_FAILURE);
1559 },
1560 true);
1561 }
1562
1563 private void clearIqCallbacks() {
1564 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1565 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1566 synchronized (this.packetCallbacks) {
1567 if (this.packetCallbacks.size() == 0) {
1568 return;
1569 }
1570 Log.d(
1571 Config.LOGTAG,
1572 account.getJid().asBareJid()
1573 + ": clearing "
1574 + this.packetCallbacks.size()
1575 + " iq callbacks");
1576 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator =
1577 this.packetCallbacks.values().iterator();
1578 while (iterator.hasNext()) {
1579 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1580 callbacks.add(entry.second);
1581 iterator.remove();
1582 }
1583 }
1584 for (OnIqPacketReceived callback : callbacks) {
1585 try {
1586 callback.onIqPacketReceived(account, failurePacket);
1587 } catch (StateChangingError error) {
1588 Log.d(
1589 Config.LOGTAG,
1590 account.getJid().asBareJid()
1591 + ": caught StateChangingError("
1592 + error.state.toString()
1593 + ") while clearing callbacks");
1594 // ignore
1595 }
1596 }
1597 Log.d(
1598 Config.LOGTAG,
1599 account.getJid().asBareJid()
1600 + ": done clearing iq callbacks. "
1601 + this.packetCallbacks.size()
1602 + " left");
1603 }
1604
1605 public void sendDiscoTimeout() {
1606 if (mWaitForDisco.compareAndSet(true, false)) {
1607 Log.d(
1608 Config.LOGTAG,
1609 account.getJid().asBareJid() + ": finalizing bind after disco timeout");
1610 finalizeBind();
1611 }
1612 }
1613
1614 private void sendStartSession() {
1615 Log.d(
1616 Config.LOGTAG,
1617 account.getJid().asBareJid() + ": sending legacy session to outdated server");
1618 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1619 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1620 this.sendUnmodifiedIqPacket(
1621 startSession,
1622 (account, packet) -> {
1623 if (packet.getType() == IqPacket.TYPE.RESULT) {
1624 final boolean waitForDisco = enableStreamManagement();
1625 sendPostBindInitialization(waitForDisco, false);
1626 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1627 throw new StateChangingError(Account.State.SESSION_FAILURE);
1628 }
1629 },
1630 true);
1631 }
1632
1633 private boolean enableStreamManagement() {
1634 final boolean streamManagement =
1635 this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
1636 if (streamManagement) {
1637 synchronized (this.mStanzaQueue) {
1638 final EnablePacket enable = new EnablePacket();
1639 tagWriter.writeStanzaAsync(enable);
1640 stanzasSent = 0;
1641 mStanzaQueue.clear();
1642 }
1643 return true;
1644 } else {
1645 return false;
1646 }
1647 }
1648
1649 private void sendPostBindInitialization(
1650 final boolean waitForDisco, final boolean carbonsEnabled) {
1651 features.carbonsEnabled = carbonsEnabled;
1652 features.blockListRequested = false;
1653 synchronized (this.disco) {
1654 this.disco.clear();
1655 }
1656 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
1657 mPendingServiceDiscoveries.set(0);
1658 if (!waitForDisco
1659 || Patches.DISCO_EXCEPTIONS.contains(
1660 account.getJid().getDomain().toEscapedString())) {
1661 Log.d(
1662 Config.LOGTAG,
1663 account.getJid().asBareJid() + ": do not wait for service discovery");
1664 mWaitForDisco.set(false);
1665 } else {
1666 mWaitForDisco.set(true);
1667 }
1668 lastDiscoStarted = SystemClock.elapsedRealtime();
1669 mXmppConnectionService.scheduleWakeUpCall(
1670 Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1671 Element caps = streamFeatures.findChild("c");
1672 final String hash = caps == null ? null : caps.getAttribute("hash");
1673 final String ver = caps == null ? null : caps.getAttribute("ver");
1674 ServiceDiscoveryResult discoveryResult = null;
1675 if (hash != null && ver != null) {
1676 discoveryResult =
1677 mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1678 }
1679 final boolean requestDiscoItemsFirst =
1680 !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
1681 if (requestDiscoItemsFirst) {
1682 sendServiceDiscoveryItems(account.getDomain());
1683 }
1684 if (discoveryResult == null) {
1685 sendServiceDiscoveryInfo(account.getDomain());
1686 } else {
1687 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
1688 disco.put(account.getDomain(), discoveryResult);
1689 }
1690 discoverMamPreferences();
1691 sendServiceDiscoveryInfo(account.getJid().asBareJid());
1692 if (!requestDiscoItemsFirst) {
1693 sendServiceDiscoveryItems(account.getDomain());
1694 }
1695
1696 if (!mWaitForDisco.get()) {
1697 finalizeBind();
1698 }
1699 this.lastSessionStarted = SystemClock.elapsedRealtime();
1700 }
1701
1702 private void sendServiceDiscoveryInfo(final Jid jid) {
1703 mPendingServiceDiscoveries.incrementAndGet();
1704 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1705 iq.setTo(jid);
1706 iq.query("http://jabber.org/protocol/disco#info");
1707 this.sendIqPacket(
1708 iq,
1709 (account, packet) -> {
1710 if (packet.getType() == IqPacket.TYPE.RESULT) {
1711 boolean advancedStreamFeaturesLoaded;
1712 synchronized (XmppConnection.this.disco) {
1713 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1714 if (jid.equals(account.getDomain())) {
1715 mXmppConnectionService.databaseBackend.insertDiscoveryResult(
1716 result);
1717 }
1718 disco.put(jid, result);
1719 advancedStreamFeaturesLoaded =
1720 disco.containsKey(account.getDomain())
1721 && disco.containsKey(account.getJid().asBareJid());
1722 }
1723 if (advancedStreamFeaturesLoaded
1724 && (jid.equals(account.getDomain())
1725 || jid.equals(account.getJid().asBareJid()))) {
1726 enableAdvancedStreamFeatures();
1727 }
1728 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
1729 Log.d(
1730 Config.LOGTAG,
1731 account.getJid().asBareJid()
1732 + ": could not query disco info for "
1733 + jid.toString());
1734 final boolean serverOrAccount =
1735 jid.equals(account.getDomain())
1736 || jid.equals(account.getJid().asBareJid());
1737 final boolean advancedStreamFeaturesLoaded;
1738 if (serverOrAccount) {
1739 synchronized (XmppConnection.this.disco) {
1740 disco.put(jid, ServiceDiscoveryResult.empty());
1741 advancedStreamFeaturesLoaded =
1742 disco.containsKey(account.getDomain())
1743 && disco.containsKey(account.getJid().asBareJid());
1744 }
1745 } else {
1746 advancedStreamFeaturesLoaded = false;
1747 }
1748 if (advancedStreamFeaturesLoaded) {
1749 enableAdvancedStreamFeatures();
1750 }
1751 }
1752 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1753 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1754 && mWaitForDisco.compareAndSet(true, false)) {
1755 finalizeBind();
1756 }
1757 }
1758 });
1759 }
1760
1761 private void discoverMamPreferences() {
1762 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1763 request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
1764 sendIqPacket(
1765 request,
1766 (account, response) -> {
1767 if (response.getType() == IqPacket.TYPE.RESULT) {
1768 Element prefs =
1769 response.findChild(
1770 "prefs", MessageArchiveService.Version.MAM_2.namespace);
1771 isMamPreferenceAlways =
1772 "always"
1773 .equals(
1774 prefs == null
1775 ? null
1776 : prefs.getAttribute("default"));
1777 }
1778 });
1779 }
1780
1781 private void discoverCommands() {
1782 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1783 request.setTo(account.getDomain());
1784 request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
1785 sendIqPacket(
1786 request,
1787 (account, response) -> {
1788 if (response.getType() == IqPacket.TYPE.RESULT) {
1789 final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
1790 if (query == null) {
1791 return;
1792 }
1793 final HashMap<String, Jid> commands = new HashMap<>();
1794 for (final Element child : query.getChildren()) {
1795 if ("item".equals(child.getName())) {
1796 final String node = child.getAttribute("node");
1797 final Jid jid = child.getAttributeAsJid("jid");
1798 if (node != null && jid != null) {
1799 commands.put(node, jid);
1800 }
1801 }
1802 }
1803 Log.d(Config.LOGTAG, commands.toString());
1804 synchronized (this.commands) {
1805 this.commands.clear();
1806 this.commands.putAll(commands);
1807 }
1808 }
1809 });
1810 }
1811
1812 public boolean isMamPreferenceAlways() {
1813 return isMamPreferenceAlways;
1814 }
1815
1816 private void finalizeBind() {
1817 Log.d(
1818 Config.LOGTAG,
1819 account.getJid().asBareJid() + ": online with resource " + account.getResource());
1820 if (bindListener != null) {
1821 bindListener.onBind(account);
1822 }
1823 changeStatus(Account.State.ONLINE);
1824 }
1825
1826 private void enableAdvancedStreamFeatures() {
1827 if (getFeatures().blocking() && !features.blockListRequested) {
1828 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
1829 this.sendIqPacket(
1830 getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1831 }
1832 for (final OnAdvancedStreamFeaturesLoaded listener :
1833 advancedStreamFeaturesLoadedListeners) {
1834 listener.onAdvancedStreamFeaturesAvailable(account);
1835 }
1836 if (getFeatures().carbons() && !features.carbonsEnabled) {
1837 sendEnableCarbons();
1838 }
1839 if (getFeatures().commands()) {
1840 discoverCommands();
1841 }
1842 }
1843
1844 private void sendServiceDiscoveryItems(final Jid server) {
1845 mPendingServiceDiscoveries.incrementAndGet();
1846 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1847 iq.setTo(server.getDomain());
1848 iq.query("http://jabber.org/protocol/disco#items");
1849 this.sendIqPacket(
1850 iq,
1851 (account, packet) -> {
1852 if (packet.getType() == IqPacket.TYPE.RESULT) {
1853 final HashSet<Jid> items = new HashSet<>();
1854 final List<Element> elements = packet.query().getChildren();
1855 for (final Element element : elements) {
1856 if (element.getName().equals("item")) {
1857 final Jid jid =
1858 InvalidJid.getNullForInvalid(
1859 element.getAttributeAsJid("jid"));
1860 if (jid != null && !jid.equals(account.getDomain())) {
1861 items.add(jid);
1862 }
1863 }
1864 }
1865 for (Jid jid : items) {
1866 sendServiceDiscoveryInfo(jid);
1867 }
1868 } else {
1869 Log.d(
1870 Config.LOGTAG,
1871 account.getJid().asBareJid()
1872 + ": could not query disco items of "
1873 + server);
1874 }
1875 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1876 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1877 && mWaitForDisco.compareAndSet(true, false)) {
1878 finalizeBind();
1879 }
1880 }
1881 });
1882 }
1883
1884 private void sendEnableCarbons() {
1885 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1886 iq.addChild("enable", Namespace.CARBONS);
1887 this.sendIqPacket(
1888 iq,
1889 (account, packet) -> {
1890 if (packet.getType() == IqPacket.TYPE.RESULT) {
1891 Log.d(
1892 Config.LOGTAG,
1893 account.getJid().asBareJid() + ": successfully enabled carbons");
1894 features.carbonsEnabled = true;
1895 } else {
1896 Log.d(
1897 Config.LOGTAG,
1898 account.getJid().asBareJid()
1899 + ": could not enable carbons "
1900 + packet);
1901 }
1902 });
1903 }
1904
1905 private void processStreamError(final Tag currentTag) throws IOException {
1906 final Element streamError = tagReader.readElement(currentTag);
1907 if (streamError == null) {
1908 return;
1909 }
1910 if (streamError.hasChild("conflict")) {
1911 account.setResource(createNewResource());
1912 Log.d(
1913 Config.LOGTAG,
1914 account.getJid().asBareJid()
1915 + ": switching resource due to conflict ("
1916 + account.getResource()
1917 + ")");
1918 throw new IOException();
1919 } else if (streamError.hasChild("host-unknown")) {
1920 throw new StateChangingException(Account.State.HOST_UNKNOWN);
1921 } else if (streamError.hasChild("policy-violation")) {
1922 this.lastConnect = SystemClock.elapsedRealtime();
1923 final String text = streamError.findChildContent("text");
1924 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
1925 failPendingMessages(text);
1926 throw new StateChangingException(Account.State.POLICY_VIOLATION);
1927 } else {
1928 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
1929 throw new StateChangingException(Account.State.STREAM_ERROR);
1930 }
1931 }
1932
1933 private void failPendingMessages(final String error) {
1934 synchronized (this.mStanzaQueue) {
1935 for (int i = 0; i < mStanzaQueue.size(); ++i) {
1936 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
1937 if (stanza instanceof MessagePacket) {
1938 final MessagePacket packet = (MessagePacket) stanza;
1939 final String id = packet.getId();
1940 final Jid to = packet.getTo();
1941 mXmppConnectionService.markMessage(
1942 account, to.asBareJid(), id, Message.STATUS_SEND_FAILED, error);
1943 }
1944 }
1945 }
1946 }
1947
1948 private void sendStartStream() throws IOException {
1949 final Tag stream = Tag.start("stream:stream");
1950 stream.setAttribute("to", account.getServer());
1951 stream.setAttribute("version", "1.0");
1952 stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
1953 stream.setAttribute("xmlns", "jabber:client");
1954 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1955 tagWriter.writeTag(stream);
1956 }
1957
1958 private String createNewResource() {
1959 return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true);
1960 }
1961
1962 private String nextRandomId() {
1963 return nextRandomId(false);
1964 }
1965
1966 private String nextRandomId(boolean s) {
1967 return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG());
1968 }
1969
1970 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1971 packet.setFrom(account.getJid());
1972 return this.sendUnmodifiedIqPacket(packet, callback, false);
1973 }
1974
1975 public synchronized String sendUnmodifiedIqPacket(
1976 final IqPacket packet, final OnIqPacketReceived callback, boolean force) {
1977 if (packet.getId() == null) {
1978 packet.setAttribute("id", nextRandomId());
1979 }
1980 if (callback != null) {
1981 synchronized (this.packetCallbacks) {
1982 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1983 }
1984 }
1985 this.sendPacket(packet, force);
1986 return packet.getId();
1987 }
1988
1989 public void sendMessagePacket(final MessagePacket packet) {
1990 this.sendPacket(packet);
1991 }
1992
1993 public void sendPresencePacket(final PresencePacket packet) {
1994 this.sendPacket(packet);
1995 }
1996
1997 private synchronized void sendPacket(final AbstractStanza packet) {
1998 sendPacket(packet, false);
1999 }
2000
2001 private synchronized void sendPacket(final AbstractStanza packet, final boolean force) {
2002 if (stanzasSent == Integer.MAX_VALUE) {
2003 resetStreamId();
2004 disconnect(true);
2005 return;
2006 }
2007 synchronized (this.mStanzaQueue) {
2008 if (force || isBound) {
2009 tagWriter.writeStanzaAsync(packet);
2010 } else {
2011 Log.d(
2012 Config.LOGTAG,
2013 account.getJid().asBareJid()
2014 + " do not write stanza to unbound stream "
2015 + packet.toString());
2016 }
2017 if (packet instanceof AbstractAcknowledgeableStanza) {
2018 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
2019
2020 if (this.mStanzaQueue.size() != 0) {
2021 int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
2022 if (currentHighestKey != stanzasSent) {
2023 throw new AssertionError("Stanza count messed up");
2024 }
2025 }
2026
2027 ++stanzasSent;
2028 this.mStanzaQueue.append(stanzasSent, stanza);
2029 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
2030 if (Config.EXTENDED_SM_LOGGING) {
2031 Log.d(
2032 Config.LOGTAG,
2033 account.getJid().asBareJid()
2034 + ": requesting ack for message stanza #"
2035 + stanzasSent);
2036 }
2037 tagWriter.writeStanzaAsync(new RequestPacket());
2038 }
2039 }
2040 }
2041 }
2042
2043 public void sendPing() {
2044 if (!r()) {
2045 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
2046 iq.setFrom(account.getJid());
2047 iq.addChild("ping", Namespace.PING);
2048 this.sendIqPacket(iq, null);
2049 }
2050 this.lastPingSent = SystemClock.elapsedRealtime();
2051 }
2052
2053 public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) {
2054 this.messageListener = listener;
2055 }
2056
2057 public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) {
2058 this.unregisteredIqListener = listener;
2059 }
2060
2061 public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) {
2062 this.presenceListener = listener;
2063 }
2064
2065 public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) {
2066 this.jingleListener = listener;
2067 }
2068
2069 public void setOnStatusChangedListener(final OnStatusChanged listener) {
2070 this.statusListener = listener;
2071 }
2072
2073 public void setOnBindListener(final OnBindListener listener) {
2074 this.bindListener = listener;
2075 }
2076
2077 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
2078 this.acknowledgedListener = listener;
2079 }
2080
2081 public void addOnAdvancedStreamFeaturesAvailableListener(
2082 final OnAdvancedStreamFeaturesLoaded listener) {
2083 this.advancedStreamFeaturesLoadedListeners.add(listener);
2084 }
2085
2086 private void forceCloseSocket() {
2087 FileBackend.close(this.socket);
2088 FileBackend.close(this.tagReader);
2089 }
2090
2091 public void interrupt() {
2092 if (this.mThread != null) {
2093 this.mThread.interrupt();
2094 }
2095 }
2096
2097 public void disconnect(final boolean force) {
2098 interrupt();
2099 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
2100 if (force) {
2101 forceCloseSocket();
2102 } else {
2103 final TagWriter currentTagWriter = this.tagWriter;
2104 if (currentTagWriter.isActive()) {
2105 currentTagWriter.finish();
2106 final Socket currentSocket = this.socket;
2107 final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch;
2108 try {
2109 currentTagWriter.await(1, TimeUnit.SECONDS);
2110 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": closing stream");
2111 currentTagWriter.writeTag(Tag.end("stream:stream"));
2112 if (streamCountDownLatch != null) {
2113 if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) {
2114 Log.d(
2115 Config.LOGTAG,
2116 account.getJid().asBareJid() + ": remote ended stream");
2117 } else {
2118 Log.d(
2119 Config.LOGTAG,
2120 account.getJid().asBareJid()
2121 + ": remote has not closed socket. force closing");
2122 }
2123 }
2124 } catch (InterruptedException e) {
2125 Log.d(
2126 Config.LOGTAG,
2127 account.getJid().asBareJid()
2128 + ": interrupted while gracefully closing stream");
2129 } catch (final IOException e) {
2130 Log.d(
2131 Config.LOGTAG,
2132 account.getJid().asBareJid()
2133 + ": io exception during disconnect ("
2134 + e.getMessage()
2135 + ")");
2136 } finally {
2137 FileBackend.close(currentSocket);
2138 }
2139 } else {
2140 forceCloseSocket();
2141 }
2142 }
2143 }
2144
2145 private void resetStreamId() {
2146 this.streamId = null;
2147 }
2148
2149 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
2150 synchronized (this.disco) {
2151 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
2152 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
2153 if (cursor.getValue().getFeatures().contains(feature)) {
2154 items.add(cursor);
2155 }
2156 }
2157 return items;
2158 }
2159 }
2160
2161 public Jid findDiscoItemByFeature(final String feature) {
2162 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
2163 if (items.size() >= 1) {
2164 return items.get(0).getKey();
2165 }
2166 return null;
2167 }
2168
2169 public boolean r() {
2170 if (getFeatures().sm()) {
2171 this.tagWriter.writeStanzaAsync(new RequestPacket());
2172 return true;
2173 } else {
2174 return false;
2175 }
2176 }
2177
2178 public List<String> getMucServersWithholdAccount() {
2179 final List<String> servers = getMucServers();
2180 servers.remove(account.getDomain().toEscapedString());
2181 return servers;
2182 }
2183
2184 public List<String> getMucServers() {
2185 List<String> servers = new ArrayList<>();
2186 synchronized (this.disco) {
2187 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
2188 final ServiceDiscoveryResult value = cursor.getValue();
2189 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
2190 && value.hasIdentity("conference", "text")
2191 && !value.getFeatures().contains("jabber:iq:gateway")
2192 && !value.hasIdentity("conference", "irc")) {
2193 servers.add(cursor.getKey().toString());
2194 }
2195 }
2196 }
2197 return servers;
2198 }
2199
2200 public String getMucServer() {
2201 List<String> servers = getMucServers();
2202 return servers.size() > 0 ? servers.get(0) : null;
2203 }
2204
2205 public int getTimeToNextAttempt() {
2206 final int additionalTime =
2207 account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
2208 final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
2209 final int secondsSinceLast =
2210 (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
2211 return interval - secondsSinceLast;
2212 }
2213
2214 public int getAttempt() {
2215 return this.attempt;
2216 }
2217
2218 public Features getFeatures() {
2219 return this.features;
2220 }
2221
2222 public long getLastSessionEstablished() {
2223 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
2224 return System.currentTimeMillis() - diff;
2225 }
2226
2227 public long getLastConnect() {
2228 return this.lastConnect;
2229 }
2230
2231 public long getLastPingSent() {
2232 return this.lastPingSent;
2233 }
2234
2235 public long getLastDiscoStarted() {
2236 return this.lastDiscoStarted;
2237 }
2238
2239 public long getLastPacketReceived() {
2240 return this.lastPacketReceived;
2241 }
2242
2243 public void sendActive() {
2244 this.sendPacket(new ActivePacket());
2245 }
2246
2247 public void sendInactive() {
2248 this.sendPacket(new InactivePacket());
2249 }
2250
2251 public void resetAttemptCount(boolean resetConnectTime) {
2252 this.attempt = 0;
2253 if (resetConnectTime) {
2254 this.lastConnect = 0;
2255 }
2256 }
2257
2258 public void setInteractive(boolean interactive) {
2259 this.mInteractive = interactive;
2260 }
2261
2262 public Identity getServerIdentity() {
2263 synchronized (this.disco) {
2264 ServiceDiscoveryResult result = disco.get(account.getJid().getDomain());
2265 if (result == null) {
2266 return Identity.UNKNOWN;
2267 }
2268 for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
2269 if (id.getType().equals("im")
2270 && id.getCategory().equals("server")
2271 && id.getName() != null) {
2272 switch (id.getName()) {
2273 case "Prosody":
2274 return Identity.PROSODY;
2275 case "ejabberd":
2276 return Identity.EJABBERD;
2277 case "Slack-XMPP":
2278 return Identity.SLACK;
2279 }
2280 }
2281 }
2282 }
2283 return Identity.UNKNOWN;
2284 }
2285
2286 private IqGenerator getIqGenerator() {
2287 return mXmppConnectionService.getIqGenerator();
2288 }
2289
2290 public enum Identity {
2291 FACEBOOK,
2292 SLACK,
2293 EJABBERD,
2294 PROSODY,
2295 NIMBUZZ,
2296 UNKNOWN
2297 }
2298
2299 private class MyKeyManager implements X509KeyManager {
2300 @Override
2301 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
2302 return account.getPrivateKeyAlias();
2303 }
2304
2305 @Override
2306 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
2307 return null;
2308 }
2309
2310 @Override
2311 public X509Certificate[] getCertificateChain(String alias) {
2312 Log.d(Config.LOGTAG, "getting certificate chain");
2313 try {
2314 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
2315 } catch (final Exception e) {
2316 Log.d(Config.LOGTAG, "could not get certificate chain", e);
2317 return new X509Certificate[0];
2318 }
2319 }
2320
2321 @Override
2322 public String[] getClientAliases(String s, Principal[] principals) {
2323 final String alias = account.getPrivateKeyAlias();
2324 return alias != null ? new String[] {alias} : new String[0];
2325 }
2326
2327 @Override
2328 public String[] getServerAliases(String s, Principal[] principals) {
2329 return new String[0];
2330 }
2331
2332 @Override
2333 public PrivateKey getPrivateKey(String alias) {
2334 try {
2335 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
2336 } catch (Exception e) {
2337 return null;
2338 }
2339 }
2340 }
2341
2342 private static class StateChangingError extends Error {
2343 private final Account.State state;
2344
2345 public StateChangingError(Account.State state) {
2346 this.state = state;
2347 }
2348 }
2349
2350 private static class StateChangingException extends IOException {
2351 private final Account.State state;
2352
2353 public StateChangingException(Account.State state) {
2354 this.state = state;
2355 }
2356 }
2357
2358 public class Features {
2359 XmppConnection connection;
2360 private boolean carbonsEnabled = false;
2361 private boolean encryptionEnabled = false;
2362 private boolean blockListRequested = false;
2363
2364 public Features(final XmppConnection connection) {
2365 this.connection = connection;
2366 }
2367
2368 private boolean hasDiscoFeature(final Jid server, final String feature) {
2369 synchronized (XmppConnection.this.disco) {
2370 final ServiceDiscoveryResult sdr = connection.disco.get(server);
2371 return sdr != null && sdr.getFeatures().contains(feature);
2372 }
2373 }
2374
2375 public boolean carbons() {
2376 return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
2377 }
2378
2379 public boolean commands() {
2380 return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
2381 }
2382
2383 public boolean easyOnboardingInvites() {
2384 synchronized (commands) {
2385 return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
2386 }
2387 }
2388
2389 public boolean bookmarksConversion() {
2390 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
2391 && pepPublishOptions();
2392 }
2393
2394 public boolean avatarConversion() {
2395 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
2396 && pepPublishOptions();
2397 }
2398
2399 public boolean blocking() {
2400 return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
2401 }
2402
2403 public boolean spamReporting() {
2404 return hasDiscoFeature(account.getDomain(), "urn:xmpp:reporting:reason:spam:0");
2405 }
2406
2407 public boolean flexibleOfflineMessageRetrieval() {
2408 return hasDiscoFeature(
2409 account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
2410 }
2411
2412 public boolean register() {
2413 return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
2414 }
2415
2416 public boolean invite() {
2417 return connection.streamFeatures != null
2418 && connection.streamFeatures.hasChild("register", Namespace.INVITE);
2419 }
2420
2421 public boolean sm() {
2422 return streamId != null
2423 || (connection.streamFeatures != null
2424 && connection.streamFeatures.hasChild("sm"));
2425 }
2426
2427 public boolean csi() {
2428 return connection.streamFeatures != null
2429 && connection.streamFeatures.hasChild("csi", Namespace.CSI);
2430 }
2431
2432 public boolean pep() {
2433 synchronized (XmppConnection.this.disco) {
2434 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2435 return info != null && info.hasIdentity("pubsub", "pep");
2436 }
2437 }
2438
2439 public boolean pepPersistent() {
2440 synchronized (XmppConnection.this.disco) {
2441 ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
2442 return info != null
2443 && info.getFeatures()
2444 .contains("http://jabber.org/protocol/pubsub#persistent-items");
2445 }
2446 }
2447
2448 public boolean pepPublishOptions() {
2449 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS);
2450 }
2451
2452 public boolean pepOmemoWhitelisted() {
2453 return hasDiscoFeature(
2454 account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED);
2455 }
2456
2457 public boolean mam() {
2458 return MessageArchiveService.Version.has(getAccountFeatures());
2459 }
2460
2461 public List<String> getAccountFeatures() {
2462 ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
2463 return result == null ? Collections.emptyList() : result.getFeatures();
2464 }
2465
2466 public boolean push() {
2467 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
2468 || hasDiscoFeature(account.getDomain(), Namespace.PUSH);
2469 }
2470
2471 public boolean rosterVersioning() {
2472 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
2473 }
2474
2475 public void setBlockListRequested(boolean value) {
2476 this.blockListRequested = value;
2477 }
2478
2479 public boolean httpUpload(long filesize) {
2480 if (Config.DISABLE_HTTP_UPLOAD) {
2481 return false;
2482 } else {
2483 for (String namespace :
2484 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2485 List<Entry<Jid, ServiceDiscoveryResult>> items =
2486 findDiscoItemsByFeature(namespace);
2487 if (items.size() > 0) {
2488 try {
2489 long maxsize =
2490 Long.parseLong(
2491 items.get(0)
2492 .getValue()
2493 .getExtendedDiscoInformation(
2494 namespace, "max-file-size"));
2495 if (filesize <= maxsize) {
2496 return true;
2497 } else {
2498 Log.d(
2499 Config.LOGTAG,
2500 account.getJid().asBareJid()
2501 + ": http upload is not available for files with size "
2502 + filesize
2503 + " (max is "
2504 + maxsize
2505 + ")");
2506 return false;
2507 }
2508 } catch (Exception e) {
2509 return true;
2510 }
2511 }
2512 }
2513 return false;
2514 }
2515 }
2516
2517 public boolean useLegacyHttpUpload() {
2518 return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
2519 && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
2520 }
2521
2522 public long getMaxHttpUploadSize() {
2523 for (String namespace :
2524 new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
2525 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
2526 if (items.size() > 0) {
2527 try {
2528 return Long.parseLong(
2529 items.get(0)
2530 .getValue()
2531 .getExtendedDiscoInformation(namespace, "max-file-size"));
2532 } catch (Exception e) {
2533 // ignored
2534 }
2535 }
2536 }
2537 return -1;
2538 }
2539
2540 public boolean stanzaIds() {
2541 return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
2542 }
2543
2544 public boolean bookmarks2() {
2545 return Config
2546 .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/;
2547 }
2548
2549 public boolean externalServiceDiscovery() {
2550 return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
2551 }
2552 }
2553}