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