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