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