1package eu.siacs.conversations.xmpp;
2
3import android.graphics.Bitmap;
4import android.graphics.BitmapFactory;
5import android.os.PowerManager;
6import android.os.PowerManager.WakeLock;
7import android.os.SystemClock;
8import android.security.KeyChain;
9import android.util.Base64;
10import android.util.Log;
11import android.util.Pair;
12import android.util.SparseArray;
13
14import org.xmlpull.v1.XmlPullParserException;
15
16import java.io.ByteArrayInputStream;
17import java.io.IOException;
18import java.io.InputStream;
19import java.math.BigInteger;
20import java.net.ConnectException;
21import java.net.IDN;
22import java.net.InetAddress;
23import java.net.InetSocketAddress;
24import java.net.Socket;
25import java.net.URL;
26import java.net.UnknownHostException;
27import java.security.KeyManagementException;
28import java.security.NoSuchAlgorithmException;
29import java.security.Principal;
30import java.security.PrivateKey;
31import java.security.cert.X509Certificate;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.HashMap;
35import java.util.HashSet;
36import java.util.Hashtable;
37import java.util.Iterator;
38import java.util.List;
39import java.util.Map.Entry;
40import java.util.concurrent.atomic.AtomicBoolean;
41import java.util.concurrent.atomic.AtomicInteger;
42import java.util.regex.Matcher;
43
44import javax.net.ssl.HostnameVerifier;
45import javax.net.ssl.KeyManager;
46import javax.net.ssl.SSLContext;
47import javax.net.ssl.SSLSession;
48import javax.net.ssl.SSLSocket;
49import javax.net.ssl.SSLSocketFactory;
50import javax.net.ssl.X509KeyManager;
51import javax.net.ssl.X509TrustManager;
52
53import de.duenndns.ssl.DomainHostnameVerifier;
54import de.duenndns.ssl.MemorizingTrustManager;
55import eu.siacs.conversations.Config;
56import eu.siacs.conversations.crypto.XmppDomainVerifier;
57import eu.siacs.conversations.crypto.sasl.Anonymous;
58import eu.siacs.conversations.crypto.sasl.DigestMd5;
59import eu.siacs.conversations.crypto.sasl.External;
60import eu.siacs.conversations.crypto.sasl.Plain;
61import eu.siacs.conversations.crypto.sasl.SaslMechanism;
62import eu.siacs.conversations.crypto.sasl.ScramSha1;
63import eu.siacs.conversations.crypto.sasl.ScramSha256;
64import eu.siacs.conversations.entities.Account;
65import eu.siacs.conversations.entities.Message;
66import eu.siacs.conversations.entities.ServiceDiscoveryResult;
67import eu.siacs.conversations.generator.IqGenerator;
68import eu.siacs.conversations.services.NotificationService;
69import eu.siacs.conversations.services.XmppConnectionService;
70import eu.siacs.conversations.utils.IP;
71import eu.siacs.conversations.utils.Patterns;
72import eu.siacs.conversations.utils.Resolver;
73import eu.siacs.conversations.utils.SSLSocketHelper;
74import eu.siacs.conversations.utils.SocksSocketFactory;
75import eu.siacs.conversations.xml.Element;
76import eu.siacs.conversations.xml.Tag;
77import eu.siacs.conversations.xml.TagWriter;
78import eu.siacs.conversations.xml.XmlReader;
79import eu.siacs.conversations.xml.Namespace;
80import eu.siacs.conversations.xmpp.forms.Data;
81import eu.siacs.conversations.xmpp.forms.Field;
82import eu.siacs.conversations.xmpp.jid.InvalidJidException;
83import eu.siacs.conversations.xmpp.jid.Jid;
84import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
85import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
86import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
87import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
88import eu.siacs.conversations.xmpp.stanzas.IqPacket;
89import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
90import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
91import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
92import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
93import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
94import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
95import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
96import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
97
98public class XmppConnection implements Runnable {
99
100 private static final int PACKET_IQ = 0;
101 private static final int PACKET_MESSAGE = 1;
102 private static final int PACKET_PRESENCE = 2;
103 protected final Account account;
104 private final WakeLock wakeLock;
105 private Socket socket;
106 private XmlReader tagReader;
107 private TagWriter tagWriter = new TagWriter();
108 private final Features features = new Features(this);
109 private boolean needsBinding = true;
110 private boolean shouldAuthenticate = true;
111 private Element streamFeatures;
112 private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
113
114 private String streamId = null;
115 private int smVersion = 3;
116 private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
117
118 private int stanzasReceived = 0;
119 private int stanzasSent = 0;
120 private long lastPacketReceived = 0;
121 private long lastPingSent = 0;
122 private long lastConnect = 0;
123 private long lastSessionStarted = 0;
124 private long lastDiscoStarted = 0;
125 private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
126 private AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
127 private AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
128 private AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
129 private boolean mInteractive = false;
130 private int attempt = 0;
131 private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
132 private OnPresencePacketReceived presenceListener = null;
133 private OnJinglePacketReceived jingleListener = null;
134 private OnIqPacketReceived unregisteredIqListener = null;
135 private OnMessagePacketReceived messageListener = null;
136 private OnStatusChanged statusListener = null;
137 private OnBindListener bindListener = null;
138 private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>();
139 private OnMessageAcknowledged acknowledgedListener = null;
140 private final XmppConnectionService mXmppConnectionService;
141
142 private SaslMechanism saslMechanism;
143 private String webRegistrationUrl = null;
144 private String verifiedHostname = null;
145
146 private class MyKeyManager implements X509KeyManager {
147 @Override
148 public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
149 return account.getPrivateKeyAlias();
150 }
151
152 @Override
153 public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
154 return null;
155 }
156
157 @Override
158 public X509Certificate[] getCertificateChain(String alias) {
159 Log.d(Config.LOGTAG,"getting certificate chain");
160 try {
161 return KeyChain.getCertificateChain(mXmppConnectionService, alias);
162 } catch (Exception e) {
163 Log.d(Config.LOGTAG,e.getMessage());
164 return new X509Certificate[0];
165 }
166 }
167
168 @Override
169 public String[] getClientAliases(String s, Principal[] principals) {
170 final String alias = account.getPrivateKeyAlias();
171 return alias != null ? new String[]{alias} : new String[0];
172 }
173
174 @Override
175 public String[] getServerAliases(String s, Principal[] principals) {
176 return new String[0];
177 }
178
179 @Override
180 public PrivateKey getPrivateKey(String alias) {
181 try {
182 return KeyChain.getPrivateKey(mXmppConnectionService, alias);
183 } catch (Exception e) {
184 return null;
185 }
186 }
187 }
188
189 public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
190 @Override
191 public void onIqPacketReceived(Account account, IqPacket packet) {
192 if (packet.getType() == IqPacket.TYPE.RESULT) {
193 account.setOption(Account.OPTION_REGISTER, false);
194 forceCloseSocket();
195 changeStatus(Account.State.REGISTRATION_SUCCESSFUL);
196 } else {
197 final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
198 "The password is too weak",
199 "Please use a longer password.");
200 Element error = packet.findChild("error");
201 Account.State state = Account.State.REGISTRATION_FAILED;
202 if (error != null) {
203 if (error.hasChild("conflict")) {
204 state = Account.State.REGISTRATION_CONFLICT;
205 } else if (error.hasChild("resource-constraint")
206 && "wait".equals(error.getAttribute("type"))) {
207 state = Account.State.REGISTRATION_PLEASE_WAIT;
208 } else if (error.hasChild("not-acceptable")
209 && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
210 state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
211 }
212 }
213 changeStatus(state);
214 forceCloseSocket();
215 }
216 }
217 };
218
219 public XmppConnection(final Account account, final XmppConnectionService service) {
220 this.account = account;
221 this.wakeLock = service.getPowerManager().newWakeLock(
222 PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
223 mXmppConnectionService = service;
224 }
225
226 protected void changeStatus(final Account.State nextStatus) {
227 synchronized (this) {
228 if (Thread.currentThread().isInterrupted()) {
229 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": not changing status to " + nextStatus + " because thread was interrupted");
230 return;
231 }
232 if (account.getStatus() != nextStatus) {
233 if ((nextStatus == Account.State.OFFLINE)
234 && (account.getStatus() != Account.State.CONNECTING)
235 && (account.getStatus() != Account.State.ONLINE)
236 && (account.getStatus() != Account.State.DISABLED)) {
237 return;
238 }
239 if (nextStatus == Account.State.ONLINE) {
240 this.attempt = 0;
241 }
242 account.setStatus(nextStatus);
243 } else {
244 return;
245 }
246 }
247 if (statusListener != null) {
248 statusListener.onStatusChanged(account);
249 }
250 }
251
252 public void prepareNewConnection() {
253 this.lastConnect = SystemClock.elapsedRealtime();
254 this.lastPingSent = SystemClock.elapsedRealtime();
255 this.lastDiscoStarted = Long.MAX_VALUE;
256 this.mWaitingForSmCatchup.set(false);
257 this.changeStatus(Account.State.CONNECTING);
258 }
259
260 public boolean isWaitingForSmCatchup() {
261 return mWaitingForSmCatchup.get();
262 }
263
264 public void incrementSmCatchupMessageCounter() {
265 this.mSmCatchupMessageCounter.incrementAndGet();
266 }
267
268 protected void connect() {
269 if (mXmppConnectionService.areMessagesInitialized()) {
270 mXmppConnectionService.resetSendingToWaiting(account);
271 }
272 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
273 features.encryptionEnabled = false;
274 this.attempt++;
275 this.verifiedHostname = null; //will be set if user entered hostname is being used or hostname was verified with dnssec
276 try {
277 Socket localSocket;
278 shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
279 this.changeStatus(Account.State.CONNECTING);
280 final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion();
281 final boolean extended = mXmppConnectionService.showExtendedConnectionOptions();
282 if (useTor) {
283 String destination;
284 if (account.getHostname().isEmpty()) {
285 destination = account.getServer().toString();
286 } else {
287 destination = account.getHostname();
288 this.verifiedHostname = destination;
289 }
290 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor");
291 localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
292 try {
293 startXmpp(localSocket);
294 } catch (InterruptedException e) {
295 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
296 return;
297 } catch (Exception e) {
298 throw new IOException(e.getMessage());
299 }
300 } else if (extended && !account.getHostname().isEmpty()) {
301
302 this.verifiedHostname = account.getHostname();
303
304 InetSocketAddress address = new InetSocketAddress(this.verifiedHostname, account.getPort());
305
306 features.encryptionEnabled = account.getPort() == 5223;
307
308 try {
309 if (features.encryptionEnabled) {
310 try {
311 final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
312 localSocket = tlsFactoryVerifier.factory.createSocket();
313 localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
314 final SSLSession session = ((SSLSocket) localSocket).getSession();
315 final String domain = account.getJid().getDomainpart();
316 if (!tlsFactoryVerifier.verifier.verify(domain, this.verifiedHostname, session)) {
317 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
318 throw new StateChangingException(Account.State.TLS_ERROR);
319 }
320 } catch (KeyManagementException e) {
321 features.encryptionEnabled = false;
322 localSocket = new Socket();
323 }
324 } else {
325 localSocket = new Socket();
326 localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
327 }
328 } catch (IOException e) {
329 throw new UnknownHostException();
330 }
331 try {
332 startXmpp(localSocket);
333 } catch (InterruptedException e) {
334 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
335 return;
336 } catch (Exception e) {
337 throw new IOException(e.getMessage());
338 }
339 } else if (IP.matches(account.getServer().toString())) {
340 localSocket = new Socket();
341 try {
342 localSocket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
343 } catch (IOException e) {
344 throw new UnknownHostException();
345 }
346 try {
347 startXmpp(localSocket);
348 } catch (InterruptedException e) {
349 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
350 return;
351 } catch (Exception e) {
352 throw new IOException(e.getMessage());
353 }
354 } else {
355 List<Resolver.Result> results = Resolver.resolve(account.getJid().getDomainpart());
356 for (Iterator<Resolver.Result> iterator = results.iterator(); iterator.hasNext(); ) {
357 final Resolver.Result result = iterator.next();
358 if (Thread.currentThread().isInterrupted()) {
359 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Thread was interrupted");
360 return;
361 }
362 try {
363 // if tls is true, encryption is implied and must not be started
364 features.encryptionEnabled = result.isDirectTls();
365 verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null;
366 final InetSocketAddress addr;
367 if (result.getIp() != null) {
368 addr = new InetSocketAddress(result.getIp(), result.getPort());
369 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
370 + ": using values from dns " + result.getHostname().toString()
371 + "/" + result.getIp().getHostAddress() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
372 } else {
373 addr = new InetSocketAddress(IDN.toASCII(result.getHostname().toString()), result.getPort());
374 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
375 + ": using values from dns "
376 + result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
377 }
378
379 if (!features.encryptionEnabled) {
380 localSocket = new Socket();
381 localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
382 } else {
383 final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
384 localSocket = tlsFactoryVerifier.factory.createSocket();
385
386 if (localSocket == null) {
387 throw new IOException("could not initialize ssl socket");
388 }
389
390 SSLSocketHelper.setSecurity((SSLSocket) localSocket);
391 SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer().getDomainpart());
392 SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client");
393
394 localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
395
396 if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) localSocket).getSession())) {
397 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
398 throw new StateChangingException(Account.State.TLS_ERROR);
399 }
400 }
401 if (startXmpp(localSocket)) {
402 break; // successfully connected to server that speaks xmpp
403 } else {
404 localSocket.close();
405 }
406 } catch (final StateChangingException e) {
407 throw e;
408 } catch (InterruptedException e) {
409 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
410 return;
411 } catch (final Throwable e) {
412 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")");
413 if (!iterator.hasNext()) {
414 throw new UnknownHostException();
415 }
416 }
417 }
418 }
419 processStream();
420 } catch (final SecurityException e) {
421 this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
422 } catch(final StateChangingException e) {
423 this.changeStatus(e.state);
424 } catch (final UnknownHostException | ConnectException e) {
425 this.changeStatus(Account.State.SERVER_NOT_FOUND);
426 } catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
427 this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
428 } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) {
429 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
430 this.changeStatus(Account.State.OFFLINE);
431 this.attempt = Math.max(0, this.attempt - 1);
432 } finally {
433 if (!Thread.currentThread().isInterrupted()) {
434 forceCloseSocket();
435 if (wakeLock.isHeld()) {
436 try {
437 wakeLock.release();
438 } catch (final RuntimeException ignored) {
439 }
440 }
441 } else {
442 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not force closing socket and releasing wake lock (is held="+wakeLock.isHeld()+") because thread was interrupted");
443 }
444 }
445 }
446
447 /**
448 * Starts xmpp protocol, call after connecting to socket
449 * @return true if server returns with valid xmpp, false otherwise
450 */
451 private boolean startXmpp(Socket socket) throws Exception {
452 if (Thread.currentThread().isInterrupted()) {
453 throw new InterruptedException();
454 }
455 this.socket = socket;
456 tagReader = new XmlReader(wakeLock);
457 if (tagWriter != null) {
458 tagWriter.forceClose();
459 }
460 tagWriter = new TagWriter();
461 tagWriter.setOutputStream(socket.getOutputStream());
462 tagReader.setInputStream(socket.getInputStream());
463 tagWriter.beginDocument();
464 sendStartStream();
465 final Tag tag = tagReader.readTag();
466 return tag != null && tag.isStart("stream");
467 }
468
469 private static class TlsFactoryVerifier {
470 private final SSLSocketFactory factory;
471 private final DomainHostnameVerifier verifier;
472
473 public TlsFactoryVerifier(final SSLSocketFactory factory, final DomainHostnameVerifier verifier) throws IOException {
474 this.factory = factory;
475 this.verifier = verifier;
476 if (factory == null || verifier == null) {
477 throw new IOException("could not setup ssl");
478 }
479 }
480 }
481
482 private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
483 final SSLContext sc = SSLSocketHelper.getSSLContext();
484 MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
485 KeyManager[] keyManager;
486 if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
487 keyManager = new KeyManager[]{new MyKeyManager()};
488 } else {
489 keyManager = null;
490 }
491 String domain = account.getJid().getDomainpart();
492 sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
493 final SSLSocketFactory factory = sc.getSocketFactory();
494 final DomainHostnameVerifier verifier = trustManager.wrapHostnameVerifier(new XmppDomainVerifier(), mInteractive);
495 return new TlsFactoryVerifier(factory, verifier);
496 }
497
498 @Override
499 public void run() {
500 forceCloseSocket();
501 connect();
502 }
503
504 private void processStream() throws XmlPullParserException, IOException, NoSuchAlgorithmException {
505 Tag nextTag = tagReader.readTag();
506 while (nextTag != null && !nextTag.isEnd("stream")) {
507 if (nextTag.isStart("error")) {
508 processStreamError(nextTag);
509 } else if (nextTag.isStart("features")) {
510 processStreamFeatures(nextTag);
511 } else if (nextTag.isStart("proceed")) {
512 switchOverToTls(nextTag);
513 } else if (nextTag.isStart("success")) {
514 final String challenge = tagReader.readElement(nextTag).getContent();
515 try {
516 saslMechanism.getResponse(challenge);
517 } catch (final SaslMechanism.AuthenticationException e) {
518 Log.e(Config.LOGTAG, String.valueOf(e));
519 throw new StateChangingException(Account.State.UNAUTHORIZED);
520 }
521 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
522 account.setKey(Account.PINNED_MECHANISM_KEY,
523 String.valueOf(saslMechanism.getPriority()));
524 tagReader.reset();
525 sendStartStream();
526 final Tag tag = tagReader.readTag();
527 if (tag != null && tag.isStart("stream")) {
528 processStream();
529 } else {
530 throw new IOException("server didn't restart stream after successful auth");
531 }
532 break;
533 } else if (nextTag.isStart("failure")) {
534 final Element failure = tagReader.readElement(nextTag);
535 if (Namespace.SASL.equals(failure.getNamespace())) {
536 final String text = failure.findChildContent("text");
537 if (failure.hasChild("account-disabled")
538 && text != null
539 && text.contains("renew")
540 && Config.MAGIC_CREATE_DOMAIN != null
541 && text.contains(Config.MAGIC_CREATE_DOMAIN)) {
542 throw new StateChangingException(Account.State.PAYMENT_REQUIRED);
543 } else {
544 throw new StateChangingException(Account.State.UNAUTHORIZED);
545 }
546 } else if (Namespace.TLS.equals(failure.getNamespace())) {
547 throw new StateChangingException(Account.State.TLS_ERROR);
548 } else {
549 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
550 }
551 } else if (nextTag.isStart("challenge")) {
552 final String challenge = tagReader.readElement(nextTag).getContent();
553 final Element response = new Element("response",Namespace.SASL);
554 try {
555 response.setContent(saslMechanism.getResponse(challenge));
556 } catch (final SaslMechanism.AuthenticationException e) {
557 // TODO: Send auth abort tag.
558 Log.e(Config.LOGTAG, e.toString());
559 }
560 tagWriter.writeElement(response);
561 } else if (nextTag.isStart("enabled")) {
562 final Element enabled = tagReader.readElement(nextTag);
563 if ("true".equals(enabled.getAttribute("resume"))) {
564 this.streamId = enabled.getAttribute("id");
565 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
566 + ": stream management(" + smVersion
567 + ") enabled (resumable)");
568 } else {
569 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
570 + ": stream management(" + smVersion + ") enabled");
571 }
572 this.stanzasReceived = 0;
573 final RequestPacket r = new RequestPacket(smVersion);
574 tagWriter.writeStanzaAsync(r);
575 } else if (nextTag.isStart("resumed")) {
576 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
577 lastPacketReceived = SystemClock.elapsedRealtime();
578 final Element resumed = tagReader.readElement(nextTag);
579 final String h = resumed.getAttribute("h");
580 try {
581 ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
582 synchronized (this.mStanzaQueue) {
583 final int serverCount = Integer.parseInt(h);
584 if (serverCount != stanzasSent) {
585 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
586 + ": session resumed with lost packages");
587 stanzasSent = serverCount;
588 } else {
589 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
590 }
591 acknowledgeStanzaUpTo(serverCount);
592 for (int i = 0; i < this.mStanzaQueue.size(); ++i) {
593 failedStanzas.add(mStanzaQueue.valueAt(i));
594 }
595 mStanzaQueue.clear();
596 }
597 Log.d(Config.LOGTAG, "resending " + failedStanzas.size() + " stanzas");
598 for (AbstractAcknowledgeableStanza packet : failedStanzas) {
599 if (packet instanceof MessagePacket) {
600 MessagePacket message = (MessagePacket) packet;
601 mXmppConnectionService.markMessage(account,
602 message.getTo().toBareJid(),
603 message.getId(),
604 Message.STATUS_UNSEND);
605 }
606 sendPacket(packet);
607 }
608 } catch (final NumberFormatException ignored) {
609 }
610 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": online with resource " + account.getResource());
611 changeStatus(Account.State.ONLINE);
612 } else if (nextTag.isStart("r")) {
613 tagReader.readElement(nextTag);
614 if (Config.EXTENDED_SM_LOGGING) {
615 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": acknowledging stanza #" + this.stanzasReceived);
616 }
617 final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
618 tagWriter.writeStanzaAsync(ack);
619 } else if (nextTag.isStart("a")) {
620 boolean accountUiNeedsRefresh = false;
621 synchronized (NotificationService.CATCHUP_LOCK) {
622 if (mWaitingForSmCatchup.compareAndSet(true, false)) {
623 int count = mSmCatchupMessageCounter.get();
624 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": SM catchup complete (" + count + ")");
625 accountUiNeedsRefresh = true;
626 if (count > 0) {
627 mXmppConnectionService.getNotificationService().finishBacklog(true, account);
628 }
629 }
630 }
631 if (accountUiNeedsRefresh) {
632 mXmppConnectionService.updateAccountUi();
633 }
634 final Element ack = tagReader.readElement(nextTag);
635 lastPacketReceived = SystemClock.elapsedRealtime();
636 try {
637 synchronized (this.mStanzaQueue) {
638 final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
639 acknowledgeStanzaUpTo(serverSequence);
640 }
641 } catch (NumberFormatException | NullPointerException e) {
642 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
643 }
644 } else if (nextTag.isStart("failed")) {
645 Element failed = tagReader.readElement(nextTag);
646 try {
647 final int serverCount = Integer.parseInt(failed.getAttribute("h"));
648 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": resumption failed but server acknowledged stanza #"+serverCount);
649 synchronized (this.mStanzaQueue) {
650 acknowledgeStanzaUpTo(serverCount);
651 }
652 } catch (NumberFormatException | NullPointerException e) {
653 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": resumption failed");
654 }
655 resetStreamId();
656 sendBindRequest();
657 } else if (nextTag.isStart("iq")) {
658 processIq(nextTag);
659 } else if (nextTag.isStart("message")) {
660 processMessage(nextTag);
661 } else if (nextTag.isStart("presence")) {
662 processPresence(nextTag);
663 }
664 nextTag = tagReader.readTag();
665 }
666 }
667
668 private void acknowledgeStanzaUpTo(int serverCount) {
669 for (int i = 0; i < mStanzaQueue.size(); ++i) {
670 if (serverCount >= mStanzaQueue.keyAt(i)) {
671 if (Config.EXTENDED_SM_LOGGING) {
672 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i));
673 }
674 AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
675 if (stanza instanceof MessagePacket && acknowledgedListener != null) {
676 MessagePacket packet = (MessagePacket) stanza;
677 acknowledgedListener.onMessageAcknowledged(account, packet.getId());
678 }
679 mStanzaQueue.removeAt(i);
680 i--;
681 }
682 }
683 }
684
685 private Element processPacket(final Tag currentTag, final int packetType)
686 throws XmlPullParserException, IOException {
687 Element element;
688 switch (packetType) {
689 case PACKET_IQ:
690 element = new IqPacket();
691 break;
692 case PACKET_MESSAGE:
693 element = new MessagePacket();
694 break;
695 case PACKET_PRESENCE:
696 element = new PresencePacket();
697 break;
698 default:
699 return null;
700 }
701 element.setAttributes(currentTag.getAttributes());
702 Tag nextTag = tagReader.readTag();
703 if (nextTag == null) {
704 throw new IOException("interrupted mid tag");
705 }
706 while (!nextTag.isEnd(element.getName())) {
707 if (!nextTag.isNo()) {
708 final Element child = tagReader.readElement(nextTag);
709 final String type = currentTag.getAttribute("type");
710 if (packetType == PACKET_IQ
711 && "jingle".equals(child.getName())
712 && ("set".equalsIgnoreCase(type) || "get"
713 .equalsIgnoreCase(type))) {
714 element = new JinglePacket();
715 element.setAttributes(currentTag.getAttributes());
716 }
717 element.addChild(child);
718 }
719 nextTag = tagReader.readTag();
720 if (nextTag == null) {
721 throw new IOException("interrupted mid tag");
722 }
723 }
724 if (stanzasReceived == Integer.MAX_VALUE) {
725 resetStreamId();
726 throw new IOException("time to restart the session. cant handle >2 billion pcks");
727 }
728 ++stanzasReceived;
729 lastPacketReceived = SystemClock.elapsedRealtime();
730 if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
731 Log.d(Config.LOGTAG,"[background stanza] "+element);
732 }
733 return element;
734 }
735
736 private void processIq(final Tag currentTag) throws XmlPullParserException, IOException {
737 final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
738
739 if (packet.getId() == null) {
740 return; // an iq packet without id is definitely invalid
741 }
742
743 if (packet instanceof JinglePacket) {
744 if (this.jingleListener != null) {
745 this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet);
746 }
747 } else {
748 OnIqPacketReceived callback = null;
749 synchronized (this.packetCallbacks) {
750 if (packetCallbacks.containsKey(packet.getId())) {
751 final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
752 // Packets to the server should have responses from the server
753 if (packetCallbackDuple.first.toServer(account)) {
754 if (packet.fromServer(account)) {
755 callback = packetCallbackDuple.second;
756 packetCallbacks.remove(packet.getId());
757 } else {
758 Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
759 }
760 } else {
761 if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
762 callback = packetCallbackDuple.second;
763 packetCallbacks.remove(packet.getId());
764 } else {
765 Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
766 }
767 }
768 } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
769 callback = this.unregisteredIqListener;
770 }
771 }
772 if (callback != null) {
773 try {
774 callback.onIqPacketReceived(account, packet);
775 } catch (StateChangingError error) {
776 throw new StateChangingException(error.state);
777 }
778 }
779 }
780 }
781
782 private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
783 final MessagePacket packet = (MessagePacket) processPacket(currentTag,PACKET_MESSAGE);
784 this.messageListener.onMessagePacketReceived(account, packet);
785 }
786
787 private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
788 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
789 this.presenceListener.onPresencePacketReceived(account, packet);
790 }
791
792 private void sendStartTLS() throws IOException {
793 final Tag startTLS = Tag.empty("starttls");
794 startTLS.setAttribute("xmlns", Namespace.TLS);
795 tagWriter.writeTag(startTLS);
796 }
797
798
799
800 private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
801 tagReader.readTag();
802 try {
803 final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
804 final InetAddress address = socket == null ? null : socket.getInetAddress();
805
806 if (address == null) {
807 throw new IOException("could not setup ssl");
808 }
809
810 final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
811
812 if (sslSocket == null) {
813 throw new IOException("could not initialize ssl socket");
814 }
815
816 SSLSocketHelper.setSecurity(sslSocket);
817
818 if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), this.verifiedHostname, sslSocket.getSession())) {
819 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed");
820 throw new StateChangingException(Account.State.TLS_ERROR);
821 }
822 tagReader.setInputStream(sslSocket.getInputStream());
823 tagWriter.setOutputStream(sslSocket.getOutputStream());
824 sendStartStream();
825 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established");
826 features.encryptionEnabled = true;
827 final Tag tag = tagReader.readTag();
828 if (tag != null && tag.isStart("stream")) {
829 processStream();
830 } else {
831 throw new IOException("server didn't restart stream after STARTTLS");
832 }
833 sslSocket.close();
834 } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
835 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
836 throw new StateChangingException(Account.State.TLS_ERROR);
837 }
838 }
839
840 private void processStreamFeatures(final Tag currentTag)
841 throws XmlPullParserException, IOException {
842 this.streamFeatures = tagReader.readElement(currentTag);
843 if (this.streamFeatures.hasChild("starttls") && !features.encryptionEnabled) {
844 sendStartTLS();
845 } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
846 if (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS) {
847 sendRegistryRequest();
848 } else {
849 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
850 }
851 } else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
852 throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
853 } else if (this.streamFeatures.hasChild("mechanisms")
854 && shouldAuthenticate
855 && (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
856 authenticate();
857 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
858 if (Config.EXTENDED_SM_LOGGING) {
859 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
860 }
861 final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
862 this.mSmCatchupMessageCounter.set(0);
863 this.mWaitingForSmCatchup.set(true);
864 this.tagWriter.writeStanzaAsync(resume);
865 } else if (needsBinding) {
866 if (this.streamFeatures.hasChild("bind")) {
867 sendBindRequest();
868 } else {
869 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
870 }
871 }
872 }
873
874 private void authenticate() throws IOException {
875 final List<String> mechanisms = extractMechanisms(streamFeatures
876 .findChild("mechanisms"));
877 final Element auth = new Element("auth",Namespace.SASL);
878 if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
879 saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
880 } else if (mechanisms.contains("SCRAM-SHA-256")) {
881 saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
882 } else if (mechanisms.contains("SCRAM-SHA-1")) {
883 saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
884 } else if (mechanisms.contains("PLAIN")) {
885 saslMechanism = new Plain(tagWriter, account);
886 } else if (mechanisms.contains("DIGEST-MD5")) {
887 saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
888 } else if (mechanisms.contains("ANONYMOUS")) {
889 saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
890 }
891 if (saslMechanism != null) {
892 final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1);
893 if (pinnedMechanism > saslMechanism.getPriority()) {
894 Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
895 " has lower priority (" + String.valueOf(saslMechanism.getPriority()) +
896 ") than pinned priority (" + pinnedMechanism +
897 "). Possible downgrade attack?");
898 throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
899 }
900 Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism());
901 auth.setAttribute("mechanism", saslMechanism.getMechanism());
902 if (!saslMechanism.getClientFirstMessage().isEmpty()) {
903 auth.setContent(saslMechanism.getClientFirstMessage());
904 }
905 tagWriter.writeElement(auth);
906 } else {
907 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
908 }
909 }
910
911 private List<String> extractMechanisms(final Element stream) {
912 final ArrayList<String> mechanisms = new ArrayList<>(stream
913 .getChildren().size());
914 for (final Element child : stream.getChildren()) {
915 mechanisms.add(child.getContent());
916 }
917 return mechanisms;
918 }
919
920 private void sendRegistryRequest() {
921 final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
922 register.query("jabber:iq:register");
923 register.setTo(account.getServer());
924 sendUnmodifiedIqPacket(register, new OnIqPacketReceived() {
925
926 @Override
927 public void onIqPacketReceived(final Account account, final IqPacket packet) {
928 boolean failed = false;
929 if (packet.getType() == IqPacket.TYPE.RESULT
930 && packet.query().hasChild("username")
931 && (packet.query().hasChild("password"))) {
932 final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
933 final Element username = new Element("username").setContent(account.getUsername());
934 final Element password = new Element("password").setContent(account.getPassword());
935 register.query("jabber:iq:register").addChild(username);
936 register.query().addChild(password);
937 register.setFrom(account.getJid().toBareJid());
938 sendUnmodifiedIqPacket(register, registrationResponseListener);
939 } else if (packet.getType() == IqPacket.TYPE.RESULT
940 && (packet.query().hasChild("x", "jabber:x:data"))) {
941 final Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
942 final Element blob = packet.query().findChild("data", "urn:xmpp:bob");
943 final String id = packet.getId();
944
945 Bitmap captcha = null;
946 if (blob != null) {
947 try {
948 final String base64Blob = blob.getContent();
949 final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
950 InputStream stream = new ByteArrayInputStream(strBlob);
951 captcha = BitmapFactory.decodeStream(stream);
952 } catch (Exception e) {
953 //ignored
954 }
955 } else {
956 try {
957 Field url = data.getFieldByName("url");
958 String urlString = url.findChildContent("value");
959 URL uri = new URL(urlString);
960 captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream());
961 } catch (IOException e) {
962 Log.e(Config.LOGTAG, e.toString());
963 }
964 }
965
966 if (captcha != null) {
967 failed = !mXmppConnectionService.displayCaptchaRequest(account, id, data, captcha);
968 }
969 } else {
970 failed = true;
971 }
972
973 if (failed) {
974 final Element query = packet.query();
975 final String instructions = query.findChildContent("instructions");
976 final Element oob = query.findChild("x",Namespace.OOB);
977 final String url = oob == null ? null : oob.findChildContent("url");
978 if (url == null && instructions != null) {
979 Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions);
980 if (matcher.find()) {
981 setAccountCreationFailed(instructions.substring(matcher.start(),matcher.end()));
982 } else {
983 setAccountCreationFailed(null);
984 }
985 } else {
986 setAccountCreationFailed(url);
987 }
988 }
989 }
990 });
991 }
992
993 private void setAccountCreationFailed(String url) {
994 if (url != null && (url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))) {
995 changeStatus(Account.State.REGISTRATION_WEB);
996 this.webRegistrationUrl = url;
997 } else {
998 changeStatus(Account.State.REGISTRATION_FAILED);
999 }
1000 disconnect(true);
1001 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": could not register. url="+url);
1002 }
1003
1004 public String getWebRegistrationUrl() {
1005 return this.webRegistrationUrl;
1006 }
1007
1008 public void resetEverything() {
1009 resetAttemptCount(true);
1010 resetStreamId();
1011 clearIqCallbacks();
1012 mStanzaQueue.clear();
1013 this.webRegistrationUrl = null;
1014 synchronized (this.disco) {
1015 disco.clear();
1016 }
1017 }
1018
1019 private void sendBindRequest() {
1020 while(!mXmppConnectionService.areMessagesInitialized() && socket != null && !socket.isClosed()) {
1021 try {
1022 Thread.sleep(500);
1023 } catch (final InterruptedException ignored) {
1024 }
1025 }
1026 needsBinding = false;
1027 clearIqCallbacks();
1028 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1029 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
1030 .addChild("resource").setContent(account.getResource());
1031 this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() {
1032 @Override
1033 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1034 if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
1035 return;
1036 }
1037 final Element bind = packet.findChild("bind");
1038 if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
1039 final Element jid = bind.findChild("jid");
1040 if (jid != null && jid.getContent() != null) {
1041 try {
1042 if (account.setJid(Jid.fromString(jid.getContent()))) {
1043 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": bare jid changed during bind. updating database");
1044 mXmppConnectionService.databaseBackend.updateAccount(account);
1045 }
1046 if (streamFeatures.hasChild("session")
1047 && !streamFeatures.findChild("session").hasChild("optional")) {
1048 sendStartSession();
1049 } else {
1050 sendPostBindInitialization();
1051 }
1052 return;
1053 } catch (final InvalidJidException e) {
1054 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server reported invalid jid ("+jid.getContent()+") on bind");
1055 }
1056 } else {
1057 Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)");
1058 }
1059 } else {
1060 Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure (" + packet.toString());
1061 }
1062 final Element error = packet.findChild("error");
1063 final String resource = account.getResource().split("\\.")[0];
1064 if (packet.getType() == IqPacket.TYPE.ERROR && error != null && error.hasChild("conflict")) {
1065 account.setResource(resource + "." + nextRandomId());
1066 } else {
1067 account.setResource(resource);
1068 }
1069 throw new StateChangingError(Account.State.BIND_FAILURE);
1070 }
1071 });
1072 }
1073
1074 private void clearIqCallbacks() {
1075 final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
1076 final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
1077 synchronized (this.packetCallbacks) {
1078 if (this.packetCallbacks.size() == 0) {
1079 return;
1080 }
1081 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
1082 final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
1083 while (iterator.hasNext()) {
1084 Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
1085 callbacks.add(entry.second);
1086 iterator.remove();
1087 }
1088 }
1089 for(OnIqPacketReceived callback : callbacks) {
1090 try {
1091 callback.onIqPacketReceived(account, failurePacket);
1092 } catch (StateChangingError error) {
1093 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": caught StateChangingError("+error.state.toString()+") while clearing callbacks");
1094 //ignore
1095 }
1096 }
1097 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done clearing iq callbacks. " + this.packetCallbacks.size() + " left");
1098 }
1099
1100 public void sendDiscoTimeout() {
1101 if (mWaitForDisco.compareAndSet(true, false)) {
1102 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": finalizing bind after disco timeout");
1103 finalizeBind();
1104 }
1105 }
1106
1107 private void sendStartSession() {
1108 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending legacy session to outdated server");
1109 final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
1110 startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
1111 this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
1112 @Override
1113 public void onIqPacketReceived(Account account, IqPacket packet) {
1114 if (packet.getType() == IqPacket.TYPE.RESULT) {
1115 sendPostBindInitialization();
1116 } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1117 throw new StateChangingError(Account.State.SESSION_FAILURE);
1118 }
1119 }
1120 });
1121 }
1122
1123 private void sendPostBindInitialization() {
1124 smVersion = 0;
1125 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
1126 smVersion = 3;
1127 } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
1128 smVersion = 2;
1129 }
1130 if (smVersion != 0) {
1131 synchronized (this.mStanzaQueue) {
1132 final EnablePacket enable = new EnablePacket(smVersion);
1133 tagWriter.writeStanzaAsync(enable);
1134 stanzasSent = 0;
1135 mStanzaQueue.clear();
1136 }
1137 }
1138 features.carbonsEnabled = false;
1139 features.blockListRequested = false;
1140 synchronized (this.disco) {
1141 this.disco.clear();
1142 }
1143 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
1144 mPendingServiceDiscoveries.set(0);
1145 if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomainpart())) {
1146 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": do not wait for service discovery");
1147 mWaitForDisco.set(false);
1148 } else {
1149 mWaitForDisco.set(true);
1150 }
1151 lastDiscoStarted = SystemClock.elapsedRealtime();
1152 mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
1153 Element caps = streamFeatures.findChild("c");
1154 final String hash = caps == null ? null : caps.getAttribute("hash");
1155 final String ver = caps == null ? null : caps.getAttribute("ver");
1156 ServiceDiscoveryResult discoveryResult = null;
1157 if (hash != null && ver != null) {
1158 discoveryResult = mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
1159 }
1160 if (discoveryResult == null) {
1161 sendServiceDiscoveryInfo(account.getServer());
1162 } else {
1163 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server caps came from cache");
1164 disco.put(account.getServer(), discoveryResult);
1165 }
1166 sendServiceDiscoveryInfo(account.getJid().toBareJid());
1167 sendServiceDiscoveryItems(account.getServer());
1168
1169 if (!mWaitForDisco.get()) {
1170 finalizeBind();
1171 }
1172 this.lastSessionStarted = SystemClock.elapsedRealtime();
1173 }
1174
1175 private void sendServiceDiscoveryInfo(final Jid jid) {
1176 mPendingServiceDiscoveries.incrementAndGet();
1177 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1178 iq.setTo(jid);
1179 iq.query("http://jabber.org/protocol/disco#info");
1180 this.sendIqPacket(iq, new OnIqPacketReceived() {
1181
1182 @Override
1183 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1184 if (packet.getType() == IqPacket.TYPE.RESULT) {
1185 boolean advancedStreamFeaturesLoaded;
1186 synchronized (XmppConnection.this.disco) {
1187 ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
1188 if (jid.equals(account.getServer())) {
1189 mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
1190 }
1191 disco.put(jid, result);
1192 advancedStreamFeaturesLoaded = disco.containsKey(account.getServer())
1193 && disco.containsKey(account.getJid().toBareJid());
1194 }
1195 if (advancedStreamFeaturesLoaded && (jid.equals(account.getServer()) || jid.equals(account.getJid().toBareJid()))) {
1196 enableAdvancedStreamFeatures();
1197 }
1198 } else {
1199 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString());
1200 }
1201 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1202 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1203 && mWaitForDisco.compareAndSet(true, false)) {
1204 finalizeBind();
1205 }
1206 }
1207 }
1208 });
1209 }
1210
1211 private void finalizeBind() {
1212 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
1213 if (bindListener != null) {
1214 bindListener.onBind(account);
1215 }
1216 changeStatus(Account.State.ONLINE);
1217 }
1218
1219 private void enableAdvancedStreamFeatures() {
1220 if (getFeatures().carbons() && !features.carbonsEnabled) {
1221 sendEnableCarbons();
1222 }
1223 if (getFeatures().blocking() && !features.blockListRequested) {
1224 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": Requesting block list");
1225 this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser());
1226 }
1227 for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
1228 listener.onAdvancedStreamFeaturesAvailable(account);
1229 }
1230 }
1231
1232 private void sendServiceDiscoveryItems(final Jid server) {
1233 mPendingServiceDiscoveries.incrementAndGet();
1234 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1235 iq.setTo(server.toDomainJid());
1236 iq.query("http://jabber.org/protocol/disco#items");
1237 this.sendIqPacket(iq, new OnIqPacketReceived() {
1238
1239 @Override
1240 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1241 if (packet.getType() == IqPacket.TYPE.RESULT) {
1242 HashSet<Jid> items = new HashSet<Jid>();
1243 final List<Element> elements = packet.query().getChildren();
1244 for (final Element element : elements) {
1245 if (element.getName().equals("item")) {
1246 final Jid jid = element.getAttributeAsJid("jid");
1247 if (jid != null && !jid.equals(account.getServer())) {
1248 items.add(jid);
1249 }
1250 }
1251 }
1252 for(Jid jid : items) {
1253 sendServiceDiscoveryInfo(jid);
1254 }
1255 } else {
1256 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco items of " + server);
1257 }
1258 if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
1259 if (mPendingServiceDiscoveries.decrementAndGet() == 0
1260 && mWaitForDisco.compareAndSet(true, false)) {
1261 finalizeBind();
1262 }
1263 }
1264 }
1265 });
1266 }
1267
1268 private void sendEnableCarbons() {
1269 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1270 iq.addChild("enable", "urn:xmpp:carbons:2");
1271 this.sendIqPacket(iq, new OnIqPacketReceived() {
1272
1273 @Override
1274 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1275 if (!packet.hasChild("error")) {
1276 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1277 + ": successfully enabled carbons");
1278 features.carbonsEnabled = true;
1279 } else {
1280 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1281 + ": error enableing carbons " + packet.toString());
1282 }
1283 }
1284 });
1285 }
1286
1287 private void processStreamError(final Tag currentTag)
1288 throws XmlPullParserException, IOException {
1289 final Element streamError = tagReader.readElement(currentTag);
1290 if (streamError == null) {
1291 return;
1292 }
1293 if (streamError.hasChild("conflict")) {
1294 final String resource = account.getResource().split("\\.")[0];
1295 account.setResource(resource + "." + nextRandomId());
1296 Log.d(Config.LOGTAG,
1297 account.getJid().toBareJid() + ": switching resource due to conflict ("
1298 + account.getResource() + ")");
1299 throw new IOException();
1300 } else if (streamError.hasChild("host-unknown")) {
1301 throw new StateChangingException(Account.State.HOST_UNKNOWN);
1302 } else if (streamError.hasChild("policy-violation")) {
1303 throw new StateChangingException(Account.State.POLICY_VIOLATION);
1304 } else {
1305 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
1306 throw new StateChangingException(Account.State.STREAM_ERROR);
1307 }
1308 }
1309
1310 private void sendStartStream() throws IOException {
1311 final Tag stream = Tag.start("stream:stream");
1312 stream.setAttribute("to", account.getServer().toString());
1313 stream.setAttribute("version", "1.0");
1314 stream.setAttribute("xml:lang", "en");
1315 stream.setAttribute("xmlns", "jabber:client");
1316 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
1317 tagWriter.writeTag(stream);
1318 }
1319
1320 private String nextRandomId() {
1321 return new BigInteger(50, mXmppConnectionService.getRNG()).toString(36);
1322 }
1323
1324 public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1325 packet.setFrom(account.getJid());
1326 return this.sendUnmodifiedIqPacket(packet, callback);
1327 }
1328
1329 public synchronized String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
1330 if (packet.getId() == null) {
1331 packet.setAttribute("id", nextRandomId());
1332 }
1333 if (callback != null) {
1334 synchronized (this.packetCallbacks) {
1335 packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
1336 }
1337 }
1338 this.sendPacket(packet);
1339 return packet.getId();
1340 }
1341
1342 public void sendMessagePacket(final MessagePacket packet) {
1343 this.sendPacket(packet);
1344 }
1345
1346 public void sendPresencePacket(final PresencePacket packet) {
1347 this.sendPacket(packet);
1348 }
1349
1350 private synchronized void sendPacket(final AbstractStanza packet) {
1351 if (stanzasSent == Integer.MAX_VALUE) {
1352 resetStreamId();
1353 disconnect(true);
1354 return;
1355 }
1356 synchronized (this.mStanzaQueue) {
1357 tagWriter.writeStanzaAsync(packet);
1358 if (packet instanceof AbstractAcknowledgeableStanza) {
1359 AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
1360 ++stanzasSent;
1361 this.mStanzaQueue.append(stanzasSent, stanza);
1362 if (stanza instanceof MessagePacket && stanza.getId() != null && getFeatures().sm()) {
1363 if (Config.EXTENDED_SM_LOGGING) {
1364 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
1365 }
1366 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
1367 }
1368 }
1369 }
1370 }
1371
1372 public void sendPing() {
1373 if (!r()) {
1374 final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
1375 iq.setFrom(account.getJid());
1376 iq.addChild("ping", "urn:xmpp:ping");
1377 this.sendIqPacket(iq, null);
1378 }
1379 this.lastPingSent = SystemClock.elapsedRealtime();
1380 }
1381
1382 public void setOnMessagePacketReceivedListener(
1383 final OnMessagePacketReceived listener) {
1384 this.messageListener = listener;
1385 }
1386
1387 public void setOnUnregisteredIqPacketReceivedListener(
1388 final OnIqPacketReceived listener) {
1389 this.unregisteredIqListener = listener;
1390 }
1391
1392 public void setOnPresencePacketReceivedListener(
1393 final OnPresencePacketReceived listener) {
1394 this.presenceListener = listener;
1395 }
1396
1397 public void setOnJinglePacketReceivedListener(
1398 final OnJinglePacketReceived listener) {
1399 this.jingleListener = listener;
1400 }
1401
1402 public void setOnStatusChangedListener(final OnStatusChanged listener) {
1403 this.statusListener = listener;
1404 }
1405
1406 public void setOnBindListener(final OnBindListener listener) {
1407 this.bindListener = listener;
1408 }
1409
1410 public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
1411 this.acknowledgedListener = listener;
1412 }
1413
1414 public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) {
1415 if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) {
1416 this.advancedStreamFeaturesLoadedListeners.add(listener);
1417 }
1418 }
1419
1420 private void forceCloseSocket() {
1421 if (socket != null) {
1422 try {
1423 socket.close();
1424 } catch (IOException e) {
1425 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception "+e.getMessage()+" during force close");
1426 }
1427 } else {
1428 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": socket was null during force close");
1429 }
1430 }
1431
1432 public void interrupt() {
1433 Thread.currentThread().interrupt();
1434 }
1435
1436 public void disconnect(final boolean force) {
1437 interrupt();
1438 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
1439 if (force) {
1440 forceCloseSocket();
1441 } else {
1442 if (tagWriter.isActive()) {
1443 tagWriter.finish();
1444 try {
1445 int i = 0;
1446 boolean warned = false;
1447 while (!tagWriter.finished() && socket.isConnected() && i <= 10) {
1448 if (!warned) {
1449 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": waiting for tag writer to finish");
1450 warned = true;
1451 }
1452 try {
1453 Thread.sleep(200);
1454 } catch(InterruptedException e) {
1455 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sleep interrupted");
1456 }
1457 i++;
1458 }
1459 if (warned) {
1460 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": tag writer has finished");
1461 }
1462 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closing stream");
1463 tagWriter.writeTag(Tag.end("stream:stream"));
1464 } catch (final IOException e) {
1465 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during disconnect ("+e.getMessage()+")");
1466 } finally {
1467 forceCloseSocket();
1468 }
1469 }
1470 }
1471 }
1472
1473 public void resetStreamId() {
1474 this.streamId = null;
1475 }
1476
1477 private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
1478 synchronized (this.disco) {
1479 final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
1480 for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
1481 if (cursor.getValue().getFeatures().contains(feature)) {
1482 items.add(cursor);
1483 }
1484 }
1485 return items;
1486 }
1487 }
1488
1489 public Jid findDiscoItemByFeature(final String feature) {
1490 final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
1491 if (items.size() >= 1) {
1492 return items.get(0).getKey();
1493 }
1494 return null;
1495 }
1496
1497 public boolean r() {
1498 if (getFeatures().sm()) {
1499 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1500 return true;
1501 } else {
1502 return false;
1503 }
1504 }
1505
1506 public String getMucServer() {
1507 synchronized (this.disco) {
1508 for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
1509 final ServiceDiscoveryResult value = cursor.getValue();
1510 if (value.getFeatures().contains("http://jabber.org/protocol/muc")
1511 && !value.getFeatures().contains("jabber:iq:gateway")
1512 && !value.hasIdentity("conference", "irc")) {
1513 return cursor.getKey().toString();
1514 }
1515 }
1516 }
1517 return null;
1518 }
1519
1520 public int getTimeToNextAttempt() {
1521 final int interval = Math.min((int) (25 * Math.pow(1.3, attempt)), 300);
1522 final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1523 return interval - secondsSinceLast;
1524 }
1525
1526 public int getAttempt() {
1527 return this.attempt;
1528 }
1529
1530 public Features getFeatures() {
1531 return this.features;
1532 }
1533
1534 public long getLastSessionEstablished() {
1535 final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1536 return System.currentTimeMillis() - diff;
1537 }
1538
1539 public long getLastConnect() {
1540 return this.lastConnect;
1541 }
1542
1543 public long getLastPingSent() {
1544 return this.lastPingSent;
1545 }
1546
1547 public long getLastDiscoStarted() {
1548 return this.lastDiscoStarted;
1549 }
1550 public long getLastPacketReceived() {
1551 return this.lastPacketReceived;
1552 }
1553
1554 public void sendActive() {
1555 this.sendPacket(new ActivePacket());
1556 }
1557
1558 public void sendInactive() {
1559 this.sendPacket(new InactivePacket());
1560 }
1561
1562 public void resetAttemptCount(boolean resetConnectTime) {
1563 this.attempt = 0;
1564 if (resetConnectTime) {
1565 this.lastConnect = 0;
1566 }
1567 }
1568
1569 public void setInteractive(boolean interactive) {
1570 this.mInteractive = interactive;
1571 }
1572
1573 public Identity getServerIdentity() {
1574 synchronized (this.disco) {
1575 ServiceDiscoveryResult result = disco.get(account.getJid().toDomainJid());
1576 if (result == null) {
1577 return Identity.UNKNOWN;
1578 }
1579 for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
1580 if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) {
1581 switch (id.getName()) {
1582 case "Prosody":
1583 return Identity.PROSODY;
1584 case "ejabberd":
1585 return Identity.EJABBERD;
1586 case "Slack-XMPP":
1587 return Identity.SLACK;
1588 }
1589 }
1590 }
1591 }
1592 return Identity.UNKNOWN;
1593 }
1594
1595 private class StateChangingError extends Error {
1596 private final Account.State state;
1597
1598 public StateChangingError(Account.State state) {
1599 this.state = state;
1600 }
1601 }
1602
1603 private class StateChangingException extends IOException {
1604 private final Account.State state;
1605
1606 public StateChangingException(Account.State state) {
1607 this.state = state;
1608 }
1609 }
1610
1611 public enum Identity {
1612 FACEBOOK,
1613 SLACK,
1614 EJABBERD,
1615 PROSODY,
1616 NIMBUZZ,
1617 UNKNOWN
1618 }
1619
1620 public class Features {
1621 XmppConnection connection;
1622 private boolean carbonsEnabled = false;
1623 private boolean encryptionEnabled = false;
1624 private boolean blockListRequested = false;
1625
1626 public Features(final XmppConnection connection) {
1627 this.connection = connection;
1628 }
1629
1630 private boolean hasDiscoFeature(final Jid server, final String feature) {
1631 synchronized (XmppConnection.this.disco) {
1632 return connection.disco.containsKey(server) &&
1633 connection.disco.get(server).getFeatures().contains(feature);
1634 }
1635 }
1636
1637 public boolean carbons() {
1638 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1639 }
1640
1641 public boolean blocking() {
1642 return hasDiscoFeature(account.getServer(), Namespace.BLOCKING);
1643 }
1644
1645 public boolean spamReporting() {
1646 return hasDiscoFeature(account.getServer(), "urn:xmpp:reporting:reason:spam:0");
1647 }
1648
1649 public boolean register() {
1650 return hasDiscoFeature(account.getServer(), Namespace.REGISTER);
1651 }
1652
1653 public boolean sm() {
1654 return streamId != null
1655 || (connection.streamFeatures != null && connection.streamFeatures.hasChild("sm"));
1656 }
1657
1658 public boolean csi() {
1659 return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1660 }
1661
1662 public boolean pep() {
1663 synchronized (XmppConnection.this.disco) {
1664 ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1665 return info != null && info.hasIdentity("pubsub", "pep");
1666 }
1667 }
1668
1669 public boolean pepPersistent() {
1670 synchronized (XmppConnection.this.disco) {
1671 ServiceDiscoveryResult info = disco.get(account.getJid().toBareJid());
1672 return info != null && info.getFeatures().contains("http://jabber.org/protocol/pubsub#persistent-items");
1673 }
1674 }
1675
1676 public boolean mam() {
1677 return hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM)
1678 || hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM_LEGACY);
1679 }
1680
1681 public boolean mamLegacy() {
1682 return !hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM)
1683 && hasDiscoFeature(account.getJid().toBareJid(), Namespace.MAM_LEGACY);
1684 }
1685
1686 public boolean push() {
1687 return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0")
1688 || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0");
1689 }
1690
1691 public boolean rosterVersioning() {
1692 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1693 }
1694
1695 public void setBlockListRequested(boolean value) {
1696 this.blockListRequested = value;
1697 }
1698
1699 public boolean httpUpload(long filesize) {
1700 if (Config.DISABLE_HTTP_UPLOAD) {
1701 return false;
1702 } else {
1703 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1704 if (items.size() > 0) {
1705 try {
1706 long maxsize = Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1707 if(filesize <= maxsize) {
1708 return true;
1709 } else {
1710 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": http upload is not available for files with size "+filesize+" (max is "+maxsize+")");
1711 return false;
1712 }
1713 } catch (Exception e) {
1714 return true;
1715 }
1716 } else {
1717 return false;
1718 }
1719 }
1720 }
1721
1722 public long getMaxHttpUploadSize() {
1723 List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(Namespace.HTTP_UPLOAD);
1724 if (items.size() > 0) {
1725 try {
1726 return Long.parseLong(items.get(0).getValue().getExtendedDiscoInformation(Namespace.HTTP_UPLOAD, "max-file-size"));
1727 } catch (Exception e) {
1728 return -1;
1729 }
1730 } else {
1731 return -1;
1732 }
1733 }
1734
1735 public boolean stanzaIds() {
1736 return hasDiscoFeature(account.getJid().toBareJid(), Namespace.STANZA_IDS);
1737 }
1738 }
1739
1740 private IqGenerator getIqGenerator() {
1741 return mXmppConnectionService.getIqGenerator();
1742 }
1743}