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