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