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