1package eu.siacs.conversations.xmpp;
2
3import android.content.Context;
4import android.content.SharedPreferences;
5import android.os.Bundle;
6import android.os.Parcelable;
7import android.os.PowerManager;
8import android.os.PowerManager.WakeLock;
9import android.os.SystemClock;
10import android.preference.PreferenceManager;
11import android.util.Log;
12import android.util.SparseArray;
13
14import org.apache.http.conn.ssl.StrictHostnameVerifier;
15import org.xmlpull.v1.XmlPullParserException;
16
17import java.io.IOException;
18import java.io.InputStream;
19import java.io.OutputStream;
20import java.math.BigInteger;
21import java.net.IDN;
22import java.net.InetSocketAddress;
23import java.net.Socket;
24import java.net.UnknownHostException;
25import java.security.KeyManagementException;
26import java.security.NoSuchAlgorithmException;
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.HashMap;
30import java.util.Hashtable;
31import java.util.LinkedList;
32import java.util.List;
33import java.util.Map.Entry;
34
35import javax.net.ssl.HostnameVerifier;
36import javax.net.ssl.SSLContext;
37import javax.net.ssl.SSLSocket;
38import javax.net.ssl.SSLSocketFactory;
39import javax.net.ssl.X509TrustManager;
40
41import eu.siacs.conversations.Config;
42import eu.siacs.conversations.entities.Account;
43import eu.siacs.conversations.services.XmppConnectionService;
44import eu.siacs.conversations.utils.CryptoHelper;
45import eu.siacs.conversations.utils.DNSHelper;
46import eu.siacs.conversations.utils.zlib.ZLibInputStream;
47import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
48import eu.siacs.conversations.xml.Element;
49import eu.siacs.conversations.xml.Tag;
50import eu.siacs.conversations.xml.TagWriter;
51import eu.siacs.conversations.xml.XmlReader;
52import eu.siacs.conversations.xmpp.jid.InvalidJidException;
53import eu.siacs.conversations.xmpp.jid.Jid;
54import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
55import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
56import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
57import eu.siacs.conversations.xmpp.stanzas.IqPacket;
58import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
59import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
60import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
61import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
62import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
63import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
64import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
65import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
66
67public class XmppConnection implements Runnable {
68
69 private static final int PACKET_IQ = 0;
70 private static final int PACKET_MESSAGE = 1;
71 private static final int PACKET_PRESENCE = 2;
72 private final Context applicationContext;
73 protected Account account;
74 private WakeLock wakeLock;
75 private Socket socket;
76 private XmlReader tagReader;
77 private TagWriter tagWriter;
78 private Features features = new Features(this);
79 private boolean shouldBind = true;
80 private boolean shouldAuthenticate = true;
81 private Element streamFeatures;
82 private HashMap<String, List<String>> disco = new HashMap<>();
83
84 private String streamId = null;
85 private int smVersion = 3;
86 private SparseArray<String> messageReceipts = new SparseArray<>();
87
88 private boolean usingCompression = false;
89 private boolean usingEncryption = false;
90 private int stanzasReceived = 0;
91 private int stanzasSent = 0;
92 private long lastPaketReceived = 0;
93 private long lastPingSent = 0;
94 private long lastConnect = 0;
95 private long lastSessionStarted = 0;
96 private int attempt = 0;
97 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<>();
98 private OnPresencePacketReceived presenceListener = null;
99 private OnJinglePacketReceived jingleListener = null;
100 private OnIqPacketReceived unregisteredIqListener = null;
101 private OnMessagePacketReceived messageListener = null;
102 private OnStatusChanged statusListener = null;
103 private OnBindListener bindListener = null;
104 private OnMessageAcknowledged acknowledgedListener = null;
105 private XmppConnectionService mXmppConnectionService = null;
106
107 public XmppConnection(Account account, XmppConnectionService service) {
108 this.account = account;
109 this.wakeLock = service.getPowerManager().newWakeLock(
110 PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
111 tagWriter = new TagWriter();
112 mXmppConnectionService = service;
113 applicationContext = service.getApplicationContext();
114 }
115
116 protected void changeStatus(int nextStatus) {
117 if (account.getStatus() != nextStatus) {
118 if ((nextStatus == Account.STATUS_OFFLINE)
119 && (account.getStatus() != Account.STATUS_CONNECTING)
120 && (account.getStatus() != Account.STATUS_ONLINE)
121 && (account.getStatus() != Account.STATUS_DISABLED)) {
122 return;
123 }
124 if (nextStatus == Account.STATUS_ONLINE) {
125 this.attempt = 0;
126 }
127 account.setStatus(nextStatus);
128 if (statusListener != null) {
129 statusListener.onStatusChanged(account);
130 }
131 }
132 }
133
134 protected void connect() {
135 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
136 usingCompression = false;
137 usingEncryption = false;
138 lastConnect = SystemClock.elapsedRealtime();
139 lastPingSent = SystemClock.elapsedRealtime();
140 this.attempt++;
141 try {
142 shouldAuthenticate = shouldBind = !account
143 .isOptionSet(Account.OPTION_REGISTER);
144 tagReader = new XmlReader(wakeLock);
145 tagWriter = new TagWriter();
146 packetCallbacks.clear();
147 this.changeStatus(Account.STATUS_CONNECTING);
148 Bundle result = DNSHelper.getSRVRecord(account.getServer());
149 ArrayList<Parcelable> values = result.getParcelableArrayList("values");
150 if ("timeout".equals(result.getString("error"))) {
151 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": dns timeout");
152 this.changeStatus(Account.STATUS_OFFLINE);
153 return;
154 } else if (values != null) {
155 int i = 0;
156 boolean socketError = true;
157 while (socketError && values.size() > i) {
158 Bundle namePort = (Bundle) values.get(i);
159 try {
160 String srvRecordServer;
161 try {
162 srvRecordServer=IDN.toASCII(namePort.getString("name"));
163 } catch (final IllegalArgumentException e) {
164 // TODO: Handle me?`
165 srvRecordServer = "";
166 }
167 int srvRecordPort = namePort.getInt("port");
168 String srvIpServer = namePort.getString("ipv4");
169 InetSocketAddress addr;
170 if (srvIpServer != null) {
171 addr = new InetSocketAddress(srvIpServer, srvRecordPort);
172 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
173 + ": using values from dns " + srvRecordServer
174 + "[" + srvIpServer + "]:" + srvRecordPort);
175 } else {
176 addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
177 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
178 + ": using values from dns "
179 + srvRecordServer + ":" + srvRecordPort);
180 }
181 socket = new Socket();
182 socket.connect(addr, 20000);
183 socketError = false;
184 } catch (UnknownHostException e) {
185 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
186 i++;
187 } catch (IOException e) {
188 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
189 i++;
190 }
191 }
192 if (socketError) {
193 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
194 if (wakeLock.isHeld()) {
195 try {
196 wakeLock.release();
197 } catch (final RuntimeException ignored) {
198 }
199 }
200 return;
201 }
202 } else if (result.containsKey("error")
203 && "nosrv".equals(result.getString("error", null))) {
204 socket = new Socket(account.getServer().getDomainpart(), 5222);
205 } else {
206 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
207 + ": timeout in DNS resolution");
208 changeStatus(Account.STATUS_OFFLINE);
209 return;
210 }
211 OutputStream out = socket.getOutputStream();
212 tagWriter.setOutputStream(out);
213 InputStream in = socket.getInputStream();
214 tagReader.setInputStream(in);
215 tagWriter.beginDocument();
216 sendStartStream();
217 Tag nextTag;
218 while ((nextTag = tagReader.readTag()) != null) {
219 if (nextTag.isStart("stream")) {
220 processStream(nextTag);
221 break;
222 } else {
223 Log.d(Config.LOGTAG,
224 "found unexpected tag: " + nextTag.getName());
225 return;
226 }
227 }
228 if (socket.isConnected()) {
229 socket.close();
230 }
231 } catch (UnknownHostException e) {
232 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
233 if (wakeLock.isHeld()) {
234 try {
235 wakeLock.release();
236 } catch (final RuntimeException ignored) {
237 }
238 }
239 } catch (final IOException | XmlPullParserException e) {
240 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
241 this.changeStatus(Account.STATUS_OFFLINE);
242 if (wakeLock.isHeld()) {
243 try {
244 wakeLock.release();
245 } catch (final RuntimeException ignored) {
246 }
247 }
248 } catch (NoSuchAlgorithmException e) {
249 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
250 this.changeStatus(Account.STATUS_OFFLINE);
251 Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
252 if (wakeLock.isHeld()) {
253 try {
254 wakeLock.release();
255 } catch (final RuntimeException ignored) {
256 }
257 }
258 }
259
260 }
261
262 @Override
263 public void run() {
264 connect();
265 }
266
267 private void processStream(final Tag currentTag) throws XmlPullParserException,
268 IOException, NoSuchAlgorithmException {
269 Tag nextTag = tagReader.readTag();
270 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
271 if (nextTag.isStart("error")) {
272 processStreamError(nextTag);
273 } else if (nextTag.isStart("features")) {
274 processStreamFeatures(nextTag);
275 } else if (nextTag.isStart("proceed")) {
276 switchOverToTls(nextTag);
277 } else if (nextTag.isStart("compressed")) {
278 switchOverToZLib(nextTag);
279 } else if (nextTag.isStart("success")) {
280 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in");
281 tagReader.readTag();
282 tagReader.reset();
283 sendStartStream();
284 processStream(tagReader.readTag());
285 break;
286 } else if (nextTag.isStart("failure")) {
287 tagReader.readElement(nextTag);
288 changeStatus(Account.STATUS_UNAUTHORIZED);
289 } else if (nextTag.isStart("challenge")) {
290 String challange = tagReader.readElement(nextTag).getContent();
291 Element response = new Element("response");
292 response.setAttribute("xmlns",
293 "urn:ietf:params:xml:ns:xmpp-sasl");
294 response.setContent(CryptoHelper.saslDigestMd5(account,
295 challange, mXmppConnectionService.getRNG()));
296 tagWriter.writeElement(response);
297 } else if (nextTag.isStart("enabled")) {
298 Element enabled = tagReader.readElement(nextTag);
299 if ("true".equals(enabled.getAttribute("resume"))) {
300 this.streamId = enabled.getAttribute("id");
301 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
302 + ": stream managment(" + smVersion
303 + ") enabled (resumable)");
304 } else {
305 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
306 + ": stream managment(" + smVersion + ") enabled");
307 }
308 this.lastSessionStarted = SystemClock.elapsedRealtime();
309 this.stanzasReceived = 0;
310 RequestPacket r = new RequestPacket(smVersion);
311 tagWriter.writeStanzaAsync(r);
312 } else if (nextTag.isStart("resumed")) {
313 lastPaketReceived = SystemClock.elapsedRealtime();
314 Element resumed = tagReader.readElement(nextTag);
315 String h = resumed.getAttribute("h");
316 try {
317 int serverCount = Integer.parseInt(h);
318 if (serverCount != stanzasSent) {
319 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
320 + ": session resumed with lost packages");
321 stanzasSent = serverCount;
322 } else {
323 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
324 + ": session resumed");
325 }
326 if (acknowledgedListener != null) {
327 for (int i = 0; i < messageReceipts.size(); ++i) {
328 if (serverCount >= messageReceipts.keyAt(i)) {
329 acknowledgedListener.onMessageAcknowledged(
330 account, messageReceipts.valueAt(i));
331 }
332 }
333 }
334 messageReceipts.clear();
335 } catch (final NumberFormatException ignored) {
336
337 }
338 sendInitialPing();
339
340 } else if (nextTag.isStart("r")) {
341 tagReader.readElement(nextTag);
342 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
343 tagWriter.writeStanzaAsync(ack);
344 } else if (nextTag.isStart("a")) {
345 Element ack = tagReader.readElement(nextTag);
346 lastPaketReceived = SystemClock.elapsedRealtime();
347 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
348 String msgId = this.messageReceipts.get(serverSequence);
349 if (msgId != null) {
350 if (this.acknowledgedListener != null) {
351 this.acknowledgedListener.onMessageAcknowledged(
352 account, msgId);
353 }
354 this.messageReceipts.remove(serverSequence);
355 }
356 } else if (nextTag.isStart("failed")) {
357 tagReader.readElement(nextTag);
358 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": resumption failed");
359 streamId = null;
360 if (account.getStatus() != Account.STATUS_ONLINE) {
361 sendBindRequest();
362 }
363 } else if (nextTag.isStart("iq")) {
364 processIq(nextTag);
365 } else if (nextTag.isStart("message")) {
366 processMessage(nextTag);
367 } else if (nextTag.isStart("presence")) {
368 processPresence(nextTag);
369 }
370 nextTag = tagReader.readTag();
371 }
372 if (account.getStatus() == Account.STATUS_ONLINE) {
373 account. setStatus(Account.STATUS_OFFLINE);
374 if (statusListener != null) {
375 statusListener.onStatusChanged(account);
376 }
377 }
378 }
379
380 private void sendInitialPing() {
381 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping");
382 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
383 iq.setFrom(account.getJid());
384 iq.addChild("ping", "urn:xmpp:ping");
385 this.sendIqPacket(iq, new OnIqPacketReceived() {
386
387 @Override
388 public void onIqPacketReceived(Account account, IqPacket packet) {
389 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
390 + ": online with resource " + account.getResource());
391 changeStatus(Account.STATUS_ONLINE);
392 }
393 });
394 }
395
396 private Element processPacket(Tag currentTag, int packetType)
397 throws XmlPullParserException, IOException {
398 Element element;
399 switch (packetType) {
400 case PACKET_IQ:
401 element = new IqPacket();
402 break;
403 case PACKET_MESSAGE:
404 element = new MessagePacket();
405 break;
406 case PACKET_PRESENCE:
407 element = new PresencePacket();
408 break;
409 default:
410 return null;
411 }
412 element.setAttributes(currentTag.getAttributes());
413 Tag nextTag = tagReader.readTag();
414 if (nextTag == null) {
415 throw new IOException("interrupted mid tag");
416 }
417 while (!nextTag.isEnd(element.getName())) {
418 if (!nextTag.isNo()) {
419 Element child = tagReader.readElement(nextTag);
420 String type = currentTag.getAttribute("type");
421 if (packetType == PACKET_IQ
422 && "jingle".equals(child.getName())
423 && ("set".equalsIgnoreCase(type) || "get"
424 .equalsIgnoreCase(type))) {
425 element = new JinglePacket();
426 element.setAttributes(currentTag.getAttributes());
427 }
428 element.addChild(child);
429 }
430 nextTag = tagReader.readTag();
431 if (nextTag == null) {
432 throw new IOException("interrupted mid tag");
433 }
434 }
435 ++stanzasReceived;
436 lastPaketReceived = SystemClock.elapsedRealtime();
437 return element;
438 }
439
440 private void processIq(Tag currentTag) throws XmlPullParserException,
441 IOException {
442 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
443
444 if (packet.getId() == null) {
445 return; // an iq packet without id is definitely invalid
446 }
447
448 if (packet instanceof JinglePacket) {
449 if (this.jingleListener != null) {
450 this.jingleListener.onJinglePacketReceived(account,
451 (JinglePacket) packet);
452 }
453 } else {
454 if (packetCallbacks.containsKey(packet.getId())) {
455 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
456 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
457 .onIqPacketReceived(account, packet);
458 }
459
460 packetCallbacks.remove(packet.getId());
461 } else if ((packet.getType() == IqPacket.TYPE_GET || packet
462 .getType() == IqPacket.TYPE_SET)
463 && this.unregisteredIqListener != null) {
464 this.unregisteredIqListener.onIqPacketReceived(account, packet);
465 }
466 }
467 }
468
469 private void processMessage(Tag currentTag) throws XmlPullParserException,
470 IOException {
471 MessagePacket packet = (MessagePacket) processPacket(currentTag,
472 PACKET_MESSAGE);
473 String id = packet.getAttribute("id");
474 if ((id != null) && (packetCallbacks.containsKey(id))) {
475 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
476 ((OnMessagePacketReceived) packetCallbacks.get(id))
477 .onMessagePacketReceived(account, packet);
478 }
479 packetCallbacks.remove(id);
480 } else if (this.messageListener != null) {
481 this.messageListener.onMessagePacketReceived(account, packet);
482 }
483 }
484
485 private void processPresence(Tag currentTag) throws XmlPullParserException,
486 IOException {
487 PresencePacket packet = (PresencePacket) processPacket(currentTag,
488 PACKET_PRESENCE);
489 String id = packet.getAttribute("id");
490 if ((id != null) && (packetCallbacks.containsKey(id))) {
491 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
492 ((OnPresencePacketReceived) packetCallbacks.get(id))
493 .onPresencePacketReceived(account, packet);
494 }
495 packetCallbacks.remove(id);
496 } else if (this.presenceListener != null) {
497 this.presenceListener.onPresencePacketReceived(account, packet);
498 }
499 }
500
501 private void sendCompressionZlib() throws IOException {
502 Element compress = new Element("compress");
503 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
504 compress.addChild("method").setContent("zlib");
505 tagWriter.writeElement(compress);
506 }
507
508 private void switchOverToZLib(final Tag currentTag)
509 throws XmlPullParserException, IOException,
510 NoSuchAlgorithmException {
511 tagReader.readTag(); // read tag close
512 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
513 .getOutputStream()));
514 tagReader
515 .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
516
517 sendStartStream();
518 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": compression enabled");
519 usingCompression = true;
520 processStream(tagReader.readTag());
521 }
522
523 private void sendStartTLS() throws IOException {
524 Tag startTLS = Tag.empty("starttls");
525 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
526 tagWriter.writeTag(startTLS);
527 }
528
529 private SharedPreferences getPreferences() {
530 return PreferenceManager
531 .getDefaultSharedPreferences(applicationContext);
532 }
533
534 private boolean enableLegacySSL() {
535 return getPreferences().getBoolean("enable_legacy_ssl", false);
536 }
537
538 private void switchOverToTls(final Tag currentTag) throws XmlPullParserException,
539 IOException {
540 tagReader.readTag();
541 try {
542 SSLContext sc = SSLContext.getInstance("TLS");
543 sc.init(null,
544 new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},
545 mXmppConnectionService.getRNG());
546 SSLSocketFactory factory = sc.getSocketFactory();
547
548 if (factory == null) {
549 throw new IOException("SSLSocketFactory was null");
550 }
551
552 final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier());
553
554 if (socket == null) {
555 throw new IOException("socket was null");
556 }
557 final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
558 socket.getInetAddress().getHostAddress(), socket.getPort(),
559 true);
560
561 // Support all protocols except legacy SSL.
562 // The min SDK version prevents us having to worry about SSLv2. In
563 // future, this may be true of SSLv3 as well.
564 final String[] supportProtocols;
565 if (enableLegacySSL()) {
566 supportProtocols = sslSocket.getSupportedProtocols();
567 } else {
568 final List<String> supportedProtocols = new LinkedList<>(
569 Arrays.asList(sslSocket.getSupportedProtocols()));
570 supportedProtocols.remove("SSLv3");
571 supportProtocols = new String[supportedProtocols.size()];
572 supportedProtocols.toArray(supportProtocols);
573 }
574 sslSocket.setEnabledProtocols(supportProtocols);
575
576 if (verifier != null
577 && !verifier.verify(account.getServer().getDomainpart(),
578 sslSocket.getSession())) {
579 sslSocket.close();
580 throw new IOException("host mismatch in TLS connection");
581 }
582 tagReader.setInputStream(sslSocket.getInputStream());
583 tagWriter.setOutputStream(sslSocket.getOutputStream());
584 sendStartStream();
585 Log.d(Config.LOGTAG, account.getJid().toBareJid()
586 + ": TLS connection established");
587 usingEncryption = true;
588 processStream(tagReader.readTag());
589 sslSocket.close();
590 } catch (final NoSuchAlgorithmException | KeyManagementException e1) {
591 e1.printStackTrace();
592 }
593 }
594
595 private void sendSaslAuthPlain() throws IOException {
596 String saslString = CryptoHelper.saslPlain(account.getUsername(),
597 account.getPassword());
598 Element auth = new Element("auth");
599 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
600 auth.setAttribute("mechanism", "PLAIN");
601 auth.setContent(saslString);
602 tagWriter.writeElement(auth);
603 }
604
605 private void sendSaslAuthDigestMd5() throws IOException {
606 Element auth = new Element("auth");
607 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
608 auth.setAttribute("mechanism", "DIGEST-MD5");
609 tagWriter.writeElement(auth);
610 }
611
612 private void processStreamFeatures(Tag currentTag)
613 throws XmlPullParserException, IOException {
614 this.streamFeatures = tagReader.readElement(currentTag);
615 if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
616 sendStartTLS();
617 } else if (compressionAvailable()) {
618 sendCompressionZlib();
619 } else if (this.streamFeatures.hasChild("register")
620 && account.isOptionSet(Account.OPTION_REGISTER)
621 && usingEncryption) {
622 sendRegistryRequest();
623 } else if (!this.streamFeatures.hasChild("register")
624 && account.isOptionSet(Account.OPTION_REGISTER)) {
625 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
626 disconnect(true);
627 } else if (this.streamFeatures.hasChild("mechanisms")
628 && shouldAuthenticate && usingEncryption) {
629 List<String> mechanisms = extractMechanisms(streamFeatures
630 .findChild("mechanisms"));
631 if (mechanisms.contains("PLAIN")) {
632 sendSaslAuthPlain();
633 } else if (mechanisms.contains("DIGEST-MD5")) {
634 sendSaslAuthDigestMd5();
635 }
636 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
637 + smVersion)
638 && streamId != null) {
639 ResumePacket resume = new ResumePacket(this.streamId,
640 stanzasReceived, smVersion);
641 this.tagWriter.writeStanzaAsync(resume);
642 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
643 sendBindRequest();
644 } else {
645 Log.d(Config.LOGTAG, account.getJid().toBareJid()
646 + ": incompatible server. disconnecting");
647 disconnect(true);
648 }
649 }
650
651 private boolean compressionAvailable() {
652 if (!this.streamFeatures.hasChild("compression",
653 "http://jabber.org/features/compress"))
654 return false;
655 if (!ZLibOutputStream.SUPPORTED)
656 return false;
657 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
658 return false;
659
660 Element compression = this.streamFeatures.findChild("compression",
661 "http://jabber.org/features/compress");
662 for (Element child : compression.getChildren()) {
663 if (!"method".equals(child.getName()))
664 continue;
665
666 if ("zlib".equalsIgnoreCase(child.getContent())) {
667 return true;
668 }
669 }
670 return false;
671 }
672
673 private List<String> extractMechanisms(Element stream) {
674 ArrayList<String> mechanisms = new ArrayList<>(stream
675 .getChildren().size());
676 for (Element child : stream.getChildren()) {
677 mechanisms.add(child.getContent());
678 }
679 return mechanisms;
680 }
681
682 private void sendRegistryRequest() {
683 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
684 register.query("jabber:iq:register");
685 register.setTo(account.getServer());
686 sendIqPacket(register, new OnIqPacketReceived() {
687
688 @Override
689 public void onIqPacketReceived(Account account, IqPacket packet) {
690 Element instructions = packet.query().findChild("instructions");
691 if (packet.query().hasChild("username")
692 && (packet.query().hasChild("password"))) {
693 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
694 Element username = new Element("username")
695 .setContent(account.getUsername());
696 Element password = new Element("password")
697 .setContent(account.getPassword());
698 register.query("jabber:iq:register").addChild(username);
699 register.query().addChild(password);
700 sendIqPacket(register, new OnIqPacketReceived() {
701
702 @Override
703 public void onIqPacketReceived(Account account,
704 IqPacket packet) {
705 if (packet.getType() == IqPacket.TYPE_RESULT) {
706 account.setOption(Account.OPTION_REGISTER,
707 false);
708 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
709 } else if (packet.hasChild("error")
710 && (packet.findChild("error")
711 .hasChild("conflict"))) {
712 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
713 } else {
714 changeStatus(Account.STATUS_REGISTRATION_FAILED);
715 Log.d(Config.LOGTAG, packet.toString());
716 }
717 disconnect(true);
718 }
719 });
720 } else {
721 changeStatus(Account.STATUS_REGISTRATION_FAILED);
722 disconnect(true);
723 Log.d(Config.LOGTAG, account.getJid().toBareJid()
724 + ": could not register. instructions are"
725 + instructions.getContent());
726 }
727 }
728 });
729 }
730
731 private void sendBindRequest() throws IOException {
732 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
733 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
734 .addChild("resource").setContent(account.getResource());
735 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
736 @Override
737 public void onIqPacketReceived(Account account, IqPacket packet) {
738 Element bind = packet.findChild("bind");
739 if (bind != null) {
740 final Element jid = bind.findChild("jid");
741 if (jid != null && jid.getContent() != null) {
742 try {
743 account.setResource(Jid.fromString(jid.getContent()).getResourcepart());
744 } catch (final InvalidJidException e) {
745 // TODO: Handle the case where an external JID is technically invalid?
746 }
747 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
748 smVersion = 3;
749 EnablePacket enable = new EnablePacket(smVersion);
750 tagWriter.writeStanzaAsync(enable);
751 stanzasSent = 0;
752 messageReceipts.clear();
753 } else if (streamFeatures.hasChild("sm",
754 "urn:xmpp:sm:2")) {
755 smVersion = 2;
756 EnablePacket enable = new EnablePacket(smVersion);
757 tagWriter.writeStanzaAsync(enable);
758 stanzasSent = 0;
759 messageReceipts.clear();
760 }
761 sendServiceDiscoveryInfo(account.getServer());
762 sendServiceDiscoveryItems(account.getServer());
763 if (bindListener != null) {
764 bindListener.onBind(account);
765 }
766 sendInitialPing();
767 } else {
768 disconnect(true);
769 }
770 } else {
771 disconnect(true);
772 }
773 }
774 });
775 if (this.streamFeatures.hasChild("session")) {
776 Log.d(Config.LOGTAG, account.getJid().toBareJid()
777 + ": sending deprecated session");
778 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
779 startSession.addChild("session",
780 "urn:ietf:params:xml:ns:xmpp-session");
781 this.sendUnboundIqPacket(startSession, null);
782 }
783 }
784
785 private void sendServiceDiscoveryInfo(final Jid server) {
786 final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
787 iq.setTo(server.toDomainJid());
788 iq.query("http://jabber.org/protocol/disco#info");
789 this.sendIqPacket(iq, new OnIqPacketReceived() {
790
791 @Override
792 public void onIqPacketReceived(Account account, IqPacket packet) {
793 final List<Element> elements = packet.query().getChildren();
794 final List<String> features = new ArrayList<>();
795 for (Element element : elements) {
796 if (element.getName().equals("feature")) {
797 features.add(element.getAttribute("var"));
798 }
799 }
800 disco.put(server.toDomainJid().toString(), features);
801
802 if (account.getServer().equals(server.toDomainJid())) {
803 enableAdvancedStreamFeatures();
804 }
805 }
806 });
807 }
808
809 private void enableAdvancedStreamFeatures() {
810 if (getFeatures().carbons()) {
811 sendEnableCarbons();
812 }
813 }
814
815 private void sendServiceDiscoveryItems(final Jid server) {
816 final IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
817 iq.setTo(server.toDomainJid());
818 iq.query("http://jabber.org/protocol/disco#items");
819 this.sendIqPacket(iq, new OnIqPacketReceived() {
820
821 @Override
822 public void onIqPacketReceived(Account account, IqPacket packet) {
823 List<Element> elements = packet.query().getChildren();
824 for (Element element : elements) {
825 if (element.getName().equals("item")) {
826 final String jid = element.getAttribute("jid");
827 try {
828 sendServiceDiscoveryInfo(Jid.fromString(jid).toDomainJid());
829 } catch (final InvalidJidException ignored) {
830 // TODO: Handle the case where an external JID is technically invalid?
831 }
832 }
833 }
834 }
835 });
836 }
837
838 private void sendEnableCarbons() {
839 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
840 iq.addChild("enable", "urn:xmpp:carbons:2");
841 this.sendIqPacket(iq, new OnIqPacketReceived() {
842
843 @Override
844 public void onIqPacketReceived(Account account, IqPacket packet) {
845 if (!packet.hasChild("error")) {
846 Log.d(Config.LOGTAG, account.getJid().toBareJid()
847 + ": successfully enabled carbons");
848 } else {
849 Log.d(Config.LOGTAG, account.getJid().toBareJid()
850 + ": error enableing carbons " + packet.toString());
851 }
852 }
853 });
854 }
855
856 private void processStreamError(Tag currentTag)
857 throws XmlPullParserException, IOException {
858 Element streamError = tagReader.readElement(currentTag);
859 if (streamError != null && streamError.hasChild("conflict")) {
860 final String resource = account.getResource().split("\\.")[0];
861 account.setResource(resource + "." + nextRandomId());
862 Log.d(Config.LOGTAG,
863 account.getJid().toBareJid() + ": switching resource due to conflict ("
864 + account.getResource() + ")");
865 }
866 }
867
868 private void sendStartStream() throws IOException {
869 Tag stream = Tag.start("stream:stream");
870 stream.setAttribute("from", account.getJid().toBareJid().toString());
871 stream.setAttribute("to", account.getServer().toString());
872 stream.setAttribute("version", "1.0");
873 stream.setAttribute("xml:lang", "en");
874 stream.setAttribute("xmlns", "jabber:client");
875 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
876 tagWriter.writeTag(stream);
877 }
878
879 private String nextRandomId() {
880 return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
881 }
882
883 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
884 if (packet.getId() == null) {
885 String id = nextRandomId();
886 packet.setAttribute("id", id);
887 }
888 packet.setFrom(account.getJid());
889 this.sendPacket(packet, callback);
890 }
891
892 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
893 if (packet.getId() == null) {
894 String id = nextRandomId();
895 packet.setAttribute("id", id);
896 }
897 this.sendPacket(packet, callback);
898 }
899
900 public void sendMessagePacket(MessagePacket packet) {
901 this.sendPacket(packet, null);
902 }
903
904 public void sendPresencePacket(PresencePacket packet) {
905 this.sendPacket(packet, null);
906 }
907
908 private synchronized void sendPacket(final AbstractStanza packet,
909 PacketReceived callback) {
910 if (packet.getName().equals("iq") || packet.getName().equals("message")
911 || packet.getName().equals("presence")) {
912 ++stanzasSent;
913 }
914 tagWriter.writeStanzaAsync(packet);
915 if (packet instanceof MessagePacket && packet.getId() != null
916 && this.streamId != null) {
917 Log.d(Config.LOGTAG, "request delivery report for stanza "
918 + stanzasSent);
919 this.messageReceipts.put(stanzasSent, packet.getId());
920 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
921 }
922 if (callback != null) {
923 if (packet.getId() == null) {
924 packet.setId(nextRandomId());
925 }
926 packetCallbacks.put(packet.getId(), callback);
927 }
928 }
929
930 public void sendPing() {
931 if (streamFeatures.hasChild("sm")) {
932 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
933 } else {
934 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
935 iq.setFrom(account.getJid());
936 iq.addChild("ping", "urn:xmpp:ping");
937 this.sendIqPacket(iq, null);
938 }
939 this.lastPingSent = SystemClock.elapsedRealtime();
940 }
941
942 public void setOnMessagePacketReceivedListener(
943 OnMessagePacketReceived listener) {
944 this.messageListener = listener;
945 }
946
947 public void setOnUnregisteredIqPacketReceivedListener(
948 OnIqPacketReceived listener) {
949 this.unregisteredIqListener = listener;
950 }
951
952 public void setOnPresencePacketReceivedListener(
953 OnPresencePacketReceived listener) {
954 this.presenceListener = listener;
955 }
956
957 public void setOnJinglePacketReceivedListener(
958 OnJinglePacketReceived listener) {
959 this.jingleListener = listener;
960 }
961
962 public void setOnStatusChangedListener(OnStatusChanged listener) {
963 this.statusListener = listener;
964 }
965
966 public void setOnBindListener(OnBindListener listener) {
967 this.bindListener = listener;
968 }
969
970 public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
971 this.acknowledgedListener = listener;
972 }
973
974 public void disconnect(boolean force) {
975 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting");
976 try {
977 if (force) {
978 socket.close();
979 return;
980 }
981 new Thread(new Runnable() {
982
983 @Override
984 public void run() {
985 if (tagWriter.isActive()) {
986 tagWriter.finish();
987 try {
988 while (!tagWriter.finished()) {
989 Log.d(Config.LOGTAG, "not yet finished");
990 Thread.sleep(100);
991 }
992 tagWriter.writeTag(Tag.end("stream:stream"));
993 socket.close();
994 } catch (IOException e) {
995 Log.d(Config.LOGTAG,
996 "io exception during disconnect");
997 } catch (InterruptedException e) {
998 Log.d(Config.LOGTAG, "interrupted");
999 }
1000 }
1001 }
1002 }).start();
1003 } catch (IOException e) {
1004 Log.d(Config.LOGTAG, "io exception during disconnect");
1005 }
1006 }
1007
1008 public List<String> findDiscoItemsByFeature(String feature) {
1009 final List<String> items = new ArrayList<>();
1010 for (Entry<String, List<String>> cursor : disco.entrySet()) {
1011 if (cursor.getValue().contains(feature)) {
1012 items.add(cursor.getKey());
1013 }
1014 }
1015 return items;
1016 }
1017
1018 public String findDiscoItemByFeature(String feature) {
1019 List<String> items = findDiscoItemsByFeature(feature);
1020 if (items.size() >= 1) {
1021 return items.get(0);
1022 }
1023 return null;
1024 }
1025
1026 public void r() {
1027 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1028 }
1029
1030 public String getMucServer() {
1031 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
1032 }
1033
1034 public int getTimeToNextAttempt() {
1035 int interval = (int) (25 * Math.pow(1.5, attempt));
1036 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1037 return interval - secondsSinceLast;
1038 }
1039
1040 public int getAttempt() {
1041 return this.attempt;
1042 }
1043
1044 public Features getFeatures() {
1045 return this.features;
1046 }
1047
1048 public long getLastSessionEstablished() {
1049 long diff;
1050 if (this.lastSessionStarted == 0) {
1051 diff = SystemClock.elapsedRealtime() - this.lastConnect;
1052 } else {
1053 diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1054 }
1055 return System.currentTimeMillis() - diff;
1056 }
1057
1058 public long getLastConnect() {
1059 return this.lastConnect;
1060 }
1061
1062 public long getLastPingSent() {
1063 return this.lastPingSent;
1064 }
1065
1066 public long getLastPacketReceived() {
1067 return this.lastPaketReceived;
1068 }
1069
1070 public void sendActive() {
1071 this.sendPacket(new ActivePacket(), null);
1072 }
1073
1074 public void sendInactive() {
1075 this.sendPacket(new InactivePacket(), null);
1076 }
1077
1078 public class Features {
1079 XmppConnection connection;
1080
1081 public Features(XmppConnection connection) {
1082 this.connection = connection;
1083 }
1084
1085 private boolean hasDiscoFeature(final Jid server, final String feature) {
1086 return connection.disco.containsKey(server.toDomainJid().toString()) &&
1087 connection.disco.get(server.toDomainJid().toString()).contains(feature);
1088 }
1089
1090 public boolean carbons() {
1091 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1092 }
1093
1094 public boolean sm() {
1095 return streamId != null;
1096 }
1097
1098 public boolean csi() {
1099 return connection.streamFeatures != null && connection.streamFeatures.hasChild("csi", "urn:xmpp:csi:0");
1100 }
1101
1102 public boolean pubsub() {
1103 return hasDiscoFeature(account.getServer(),
1104 "http://jabber.org/protocol/pubsub#publish");
1105 }
1106
1107 public boolean mam() {
1108 return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1109 }
1110
1111 public boolean rosterVersioning() {
1112 return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver");
1113 }
1114
1115 public boolean streamhost() {
1116 return connection
1117 .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1118 }
1119
1120 public boolean compression() {
1121 return connection.usingCompression;
1122 }
1123 }
1124}