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.KeyStore;
11import java.security.KeyStoreException;
12import java.security.MessageDigest;
13import java.security.NoSuchAlgorithmException;
14import java.security.SecureRandom;
15import java.security.cert.CertPathValidatorException;
16import java.security.cert.CertificateException;
17import java.security.cert.X509Certificate;
18import java.util.ArrayList;
19import java.util.HashMap;
20import java.util.Hashtable;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Map.Entry;
24
25import javax.net.ssl.SSLContext;
26import javax.net.ssl.SSLSocket;
27import javax.net.ssl.SSLSocketFactory;
28import javax.net.ssl.TrustManager;
29import javax.net.ssl.TrustManagerFactory;
30import javax.net.ssl.X509TrustManager;
31
32import org.xmlpull.v1.XmlPullParserException;
33
34import android.os.Bundle;
35import android.os.PowerManager;
36import android.os.PowerManager.WakeLock;
37import android.os.SystemClock;
38import android.util.Log;
39import eu.siacs.conversations.entities.Account;
40import eu.siacs.conversations.services.XmppConnectionService;
41import eu.siacs.conversations.utils.CryptoHelper;
42import eu.siacs.conversations.utils.DNSHelper;
43import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
44import eu.siacs.conversations.utils.zlib.ZLibInputStream;
45import eu.siacs.conversations.xml.Element;
46import eu.siacs.conversations.xml.Tag;
47import eu.siacs.conversations.xml.TagWriter;
48import eu.siacs.conversations.xml.XmlReader;
49import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
50import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
51import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
52import eu.siacs.conversations.xmpp.stanzas.IqPacket;
53import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
54import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
55import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
56import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
57import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
58import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
59
60public class XmppConnection implements Runnable {
61
62 protected Account account;
63 private static final String LOGTAG = "xmppService";
64
65 private WakeLock wakeLock;
66
67 private SecureRandom mRandom;
68
69 private Socket socket;
70 private XmlReader tagReader;
71 private TagWriter tagWriter;
72
73 private boolean shouldBind = true;
74 private boolean shouldAuthenticate = true;
75 private Element streamFeatures;
76 private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
77
78 private String streamId = null;
79 private int smVersion = 3;
80
81 private int stanzasReceived = 0;
82 private int stanzasSent = 0;
83
84 public long lastPaketReceived = 0;
85 public long lastPingSent = 0;
86 public long lastConnect = 0;
87 public long lastSessionStarted = 0;
88
89 private int attempt = 0;
90
91 private static final int PACKET_IQ = 0;
92 private static final int PACKET_MESSAGE = 1;
93 private static final int PACKET_PRESENCE = 2;
94
95 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
96 private OnPresencePacketReceived presenceListener = null;
97 private OnJinglePacketReceived jingleListener = null;
98 private OnIqPacketReceived unregisteredIqListener = null;
99 private OnMessagePacketReceived messageListener = null;
100 private OnStatusChanged statusListener = null;
101 private OnTLSExceptionReceived tlsListener = null;
102 private OnBindListener bindListener = null;
103
104 public XmppConnection(Account account, XmppConnectionService service) {
105 this.mRandom = service.getRNG();
106 this.account = account;
107 this.wakeLock = service.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
108 account.getJid());
109 tagWriter = new TagWriter();
110 }
111
112 protected void changeStatus(int nextStatus) {
113 if (account.getStatus() != nextStatus) {
114 if ((nextStatus == Account.STATUS_OFFLINE)
115 && (account.getStatus() != Account.STATUS_CONNECTING)
116 && (account.getStatus() != Account.STATUS_ONLINE)
117 && (account.getStatus() != Account.STATUS_DISABLED)) {
118 return;
119 }
120 if (nextStatus == Account.STATUS_ONLINE) {
121 this.attempt = 0;
122 }
123 account.setStatus(nextStatus);
124 if (statusListener != null) {
125 statusListener.onStatusChanged(account);
126 }
127 }
128 }
129
130 protected void connect() {
131 Log.d(LOGTAG, account.getJid() + ": connecting");
132 lastConnect = SystemClock.elapsedRealtime();
133 this.attempt++;
134 try {
135 shouldAuthenticate = shouldBind = !account
136 .isOptionSet(Account.OPTION_REGISTER);
137 tagReader = new XmlReader(wakeLock);
138 tagWriter = new TagWriter();
139 packetCallbacks.clear();
140 this.changeStatus(Account.STATUS_CONNECTING);
141 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
142 if ("timeout".equals(namePort.getString("error"))) {
143 Log.d(LOGTAG, account.getJid() + ": dns timeout");
144 this.changeStatus(Account.STATUS_OFFLINE);
145 return;
146 }
147 String srvRecordServer = namePort.getString("name");
148 String srvIpServer = namePort.getString("ipv4");
149 int srvRecordPort = namePort.getInt("port");
150 if (srvRecordServer != null) {
151 if (srvIpServer != null) {
152 Log.d(LOGTAG, account.getJid() + ": using values from dns "
153 + srvRecordServer + "[" + srvIpServer + "]:"
154 + srvRecordPort);
155 socket = new Socket(srvIpServer, srvRecordPort);
156 } else {
157 Log.d(LOGTAG, account.getJid() + ": using values from dns "
158 + srvRecordServer + ":" + srvRecordPort);
159 socket = new Socket(srvRecordServer, srvRecordPort);
160 }
161 } else {
162 socket = new Socket(account.getServer(), 5222);
163 }
164 OutputStream out = socket.getOutputStream();
165 tagWriter.setOutputStream(out);
166 InputStream in = socket.getInputStream();
167 tagReader.setInputStream(in);
168 tagWriter.beginDocument();
169 sendStartStream();
170 Tag nextTag;
171 while ((nextTag = tagReader.readTag()) != null) {
172 if (nextTag.isStart("stream")) {
173 processStream(nextTag);
174 break;
175 } else {
176 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
177 return;
178 }
179 }
180 if (socket.isConnected()) {
181 socket.close();
182 }
183 } catch (UnknownHostException e) {
184 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
185 if (wakeLock.isHeld()) {
186 try { wakeLock.release();} catch (RuntimeException re) {}
187 }
188 return;
189 } catch (IOException e) {
190 if (account.getStatus() != Account.STATUS_TLS_ERROR) {
191 this.changeStatus(Account.STATUS_OFFLINE);
192 }
193 if (wakeLock.isHeld()) {
194 try { wakeLock.release();} catch (RuntimeException re) {}
195 }
196 return;
197 } catch (NoSuchAlgorithmException e) {
198 this.changeStatus(Account.STATUS_OFFLINE);
199 Log.d(LOGTAG, "compression exception " + e.getMessage());
200 if (wakeLock.isHeld()) {
201 try { wakeLock.release();} catch (RuntimeException re) {}
202 }
203 return;
204 } catch (XmlPullParserException e) {
205 this.changeStatus(Account.STATUS_OFFLINE);
206 Log.d(LOGTAG, "xml exception " + e.getMessage());
207 if (wakeLock.isHeld()) {
208 try { wakeLock.release();} catch (RuntimeException re) {}
209 }
210 return;
211 }
212
213 }
214
215 @Override
216 public void run() {
217 connect();
218 }
219
220 private void processStream(Tag currentTag) throws XmlPullParserException,
221 IOException, NoSuchAlgorithmException {
222 Tag nextTag = tagReader.readTag();
223 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
224 if (nextTag.isStart("error")) {
225 processStreamError(nextTag);
226 } else if (nextTag.isStart("features")) {
227 processStreamFeatures(nextTag);
228 if ((streamFeatures.getChildren().size() == 1)
229 && (streamFeatures.hasChild("starttls"))
230 && (!account.isOptionSet(Account.OPTION_USETLS))) {
231 changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
232 }
233 } else if (nextTag.isStart("proceed")) {
234 switchOverToTls(nextTag);
235 } else if (nextTag.isStart("compressed")) {
236 switchOverToZLib(nextTag);
237 } else if (nextTag.isStart("success")) {
238 Log.d(LOGTAG, account.getJid() + ": logged in");
239 tagReader.readTag();
240 tagReader.reset();
241 sendStartStream();
242 processStream(tagReader.readTag());
243 break;
244 } else if (nextTag.isStart("failure")) {
245 tagReader.readElement(nextTag);
246 changeStatus(Account.STATUS_UNAUTHORIZED);
247 } else if (nextTag.isStart("challenge")) {
248 String challange = tagReader.readElement(nextTag).getContent();
249 Element response = new Element("response");
250 response.setAttribute("xmlns",
251 "urn:ietf:params:xml:ns:xmpp-sasl");
252 response.setContent(CryptoHelper.saslDigestMd5(account,
253 challange,mRandom));
254 tagWriter.writeElement(response);
255 } else if (nextTag.isStart("enabled")) {
256 this.stanzasSent = 0;
257 Element enabled = tagReader.readElement(nextTag);
258 if ("true".equals(enabled.getAttribute("resume"))) {
259 this.streamId = enabled.getAttribute("id");
260 Log.d(LOGTAG, account.getJid() + ": stream managment("
261 + smVersion + ") enabled (resumable)");
262 } else {
263 Log.d(LOGTAG, account.getJid() + ": stream managment("
264 + smVersion + ") enabled");
265 }
266 this.lastSessionStarted = SystemClock.elapsedRealtime();
267 this.stanzasReceived = 0;
268 RequestPacket r = new RequestPacket(smVersion);
269 tagWriter.writeStanzaAsync(r);
270 } else if (nextTag.isStart("resumed")) {
271 lastPaketReceived = SystemClock.elapsedRealtime();
272 Log.d(LOGTAG, account.getJid() + ": session resumed");
273 tagReader.readElement(nextTag);
274 sendPing();
275 changeStatus(Account.STATUS_ONLINE);
276 } else if (nextTag.isStart("r")) {
277 tagReader.readElement(nextTag);
278 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
279 tagWriter.writeStanzaAsync(ack);
280 } else if (nextTag.isStart("a")) {
281 Element ack = tagReader.readElement(nextTag);
282 lastPaketReceived = SystemClock.elapsedRealtime();
283 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
284 if (serverSequence > this.stanzasSent) {
285 this.stanzasSent = serverSequence;
286 }
287 } else if (nextTag.isStart("failed")) {
288 tagReader.readElement(nextTag);
289 Log.d(LOGTAG, account.getJid() + ": resumption failed");
290 streamId = null;
291 if (account.getStatus() != Account.STATUS_ONLINE) {
292 sendBindRequest();
293 }
294 } else if (nextTag.isStart("iq")) {
295 processIq(nextTag);
296 } else if (nextTag.isStart("message")) {
297 processMessage(nextTag);
298 } else if (nextTag.isStart("presence")) {
299 processPresence(nextTag);
300 }
301 nextTag = tagReader.readTag();
302 }
303 if (account.getStatus() == Account.STATUS_ONLINE) {
304 account.setStatus(Account.STATUS_OFFLINE);
305 if (statusListener != null) {
306 statusListener.onStatusChanged(account);
307 }
308 }
309 }
310
311 private Element processPacket(Tag currentTag, int packetType)
312 throws XmlPullParserException, IOException {
313 Element element;
314 switch (packetType) {
315 case PACKET_IQ:
316 element = new IqPacket();
317 break;
318 case PACKET_MESSAGE:
319 element = new MessagePacket();
320 break;
321 case PACKET_PRESENCE:
322 element = new PresencePacket();
323 break;
324 default:
325 return null;
326 }
327 element.setAttributes(currentTag.getAttributes());
328 Tag nextTag = tagReader.readTag();
329 if (nextTag==null) {
330 throw new IOException("interrupted mid tag");
331 }
332 while (!nextTag.isEnd(element.getName())) {
333 if (!nextTag.isNo()) {
334 Element child = tagReader.readElement(nextTag);
335 if ((packetType == PACKET_IQ)
336 && ("jingle".equals(child.getName()))) {
337 element = new JinglePacket();
338 element.setAttributes(currentTag.getAttributes());
339 }
340 element.addChild(child);
341 }
342 nextTag = tagReader.readTag();
343 if (nextTag==null) {
344 throw new IOException("interrupted mid tag");
345 }
346 }
347 ++stanzasReceived;
348 lastPaketReceived = SystemClock.elapsedRealtime();
349 return element;
350 }
351
352 private void processIq(Tag currentTag) throws XmlPullParserException,
353 IOException {
354 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
355
356 if (packet.getId() == null) {
357 return; // an iq packet without id is definitely invalid
358 }
359
360 if (packet instanceof JinglePacket) {
361 if (this.jingleListener != null) {
362 this.jingleListener.onJinglePacketReceived(account,
363 (JinglePacket) packet);
364 }
365 } else {
366 if (packetCallbacks.containsKey(packet.getId())) {
367 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
368 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
369 .onIqPacketReceived(account, packet);
370 }
371
372 packetCallbacks.remove(packet.getId());
373 } else if (this.unregisteredIqListener != null) {
374 this.unregisteredIqListener.onIqPacketReceived(account, packet);
375 }
376 }
377 }
378
379 private void processMessage(Tag currentTag) throws XmlPullParserException,
380 IOException {
381 MessagePacket packet = (MessagePacket) processPacket(currentTag,
382 PACKET_MESSAGE);
383 String id = packet.getAttribute("id");
384 if ((id != null) && (packetCallbacks.containsKey(id))) {
385 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
386 ((OnMessagePacketReceived) packetCallbacks.get(id))
387 .onMessagePacketReceived(account, packet);
388 }
389 packetCallbacks.remove(id);
390 } else if (this.messageListener != null) {
391 this.messageListener.onMessagePacketReceived(account, packet);
392 }
393 }
394
395 private void processPresence(Tag currentTag) throws XmlPullParserException,
396 IOException {
397 PresencePacket packet = (PresencePacket) processPacket(currentTag,
398 PACKET_PRESENCE);
399 String id = packet.getAttribute("id");
400 if ((id != null) && (packetCallbacks.containsKey(id))) {
401 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
402 ((OnPresencePacketReceived) packetCallbacks.get(id))
403 .onPresencePacketReceived(account, packet);
404 }
405 packetCallbacks.remove(id);
406 } else if (this.presenceListener != null) {
407 this.presenceListener.onPresencePacketReceived(account, packet);
408 }
409 }
410
411 private void sendCompressionZlib() throws IOException {
412 Element compress = new Element("compress");
413 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
414 compress.addChild("method").setContent("zlib");
415 tagWriter.writeElement(compress);
416 }
417
418 private void switchOverToZLib(Tag currentTag)
419 throws XmlPullParserException, IOException,
420 NoSuchAlgorithmException {
421 tagReader.readTag(); // read tag close
422
423 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
424 .getOutputStream()));
425 tagReader
426 .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
427
428 sendStartStream();
429 Log.d(LOGTAG, account.getJid() + ": compression enabled");
430 processStream(tagReader.readTag());
431 }
432
433 private void sendStartTLS() throws IOException {
434 Tag startTLS = Tag.empty("starttls");
435 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
436 tagWriter.writeTag(startTLS);
437 }
438
439 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
440 IOException {
441 tagReader.readTag();
442 try {
443 SSLContext sc = SSLContext.getInstance("TLS");
444 TrustManagerFactory tmf = TrustManagerFactory
445 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
446 try {
447 tmf.init((KeyStore) null);
448 } catch (KeyStoreException e1) {
449 e1.printStackTrace();
450 }
451
452 TrustManager[] trustManagers = tmf.getTrustManagers();
453 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
454
455 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
456
457 @Override
458 public void checkClientTrusted(X509Certificate[] chain,
459 String authType) throws CertificateException {
460 origTrustmanager.checkClientTrusted(chain, authType);
461 }
462
463 @Override
464 public void checkServerTrusted(X509Certificate[] chain,
465 String authType) throws CertificateException {
466 try {
467 origTrustmanager.checkServerTrusted(chain, authType);
468 } catch (CertificateException e) {
469 if (e.getCause() instanceof CertPathValidatorException) {
470 String sha;
471 try {
472 MessageDigest sha1 = MessageDigest
473 .getInstance("SHA1");
474 sha1.update(chain[0].getEncoded());
475 sha = CryptoHelper.bytesToHex(sha1.digest());
476 if (!sha.equals(account.getSSLFingerprint())) {
477 changeStatus(Account.STATUS_TLS_ERROR);
478 if (tlsListener != null) {
479 tlsListener.onTLSExceptionReceived(sha,
480 account);
481 }
482 throw new CertificateException();
483 }
484 } catch (NoSuchAlgorithmException e1) {
485 // TODO Auto-generated catch block
486 e1.printStackTrace();
487 }
488 } else {
489 throw new CertificateException();
490 }
491 }
492 }
493
494 @Override
495 public X509Certificate[] getAcceptedIssuers() {
496 return origTrustmanager.getAcceptedIssuers();
497 }
498
499 } };
500 sc.init(null, wrappedTrustManagers, null);
501 SSLSocketFactory factory = sc.getSocketFactory();
502 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
503 socket.getInetAddress().getHostAddress(), socket.getPort(),
504 true);
505 tagReader.setInputStream(sslSocket.getInputStream());
506 tagWriter.setOutputStream(sslSocket.getOutputStream());
507 sendStartStream();
508 Log.d(LOGTAG, account.getJid() + ": TLS connection established");
509 processStream(tagReader.readTag());
510 sslSocket.close();
511 } catch (NoSuchAlgorithmException e1) {
512 // TODO Auto-generated catch block
513 e1.printStackTrace();
514 } catch (KeyManagementException e) {
515 // TODO Auto-generated catch block
516 e.printStackTrace();
517 }
518 }
519
520 private void sendSaslAuthPlain() throws IOException {
521 String saslString = CryptoHelper.saslPlain(account.getUsername(),
522 account.getPassword());
523 Element auth = new Element("auth");
524 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
525 auth.setAttribute("mechanism", "PLAIN");
526 auth.setContent(saslString);
527 tagWriter.writeElement(auth);
528 }
529
530 private void sendSaslAuthDigestMd5() throws IOException {
531 Element auth = new Element("auth");
532 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
533 auth.setAttribute("mechanism", "DIGEST-MD5");
534 tagWriter.writeElement(auth);
535 }
536
537 private void processStreamFeatures(Tag currentTag)
538 throws XmlPullParserException, IOException {
539 this.streamFeatures = tagReader.readElement(currentTag);
540 if (this.streamFeatures.hasChild("starttls")
541 && account.isOptionSet(Account.OPTION_USETLS)) {
542 sendStartTLS();
543 } else if (compressionAvailable()) {
544 sendCompressionZlib();
545 } else if (this.streamFeatures.hasChild("register")
546 && (account.isOptionSet(Account.OPTION_REGISTER))) {
547 sendRegistryRequest();
548 } else if (!this.streamFeatures.hasChild("register")
549 && (account.isOptionSet(Account.OPTION_REGISTER))) {
550 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
551 disconnect(true);
552 } else if (this.streamFeatures.hasChild("mechanisms")
553 && shouldAuthenticate) {
554 List<String> mechanisms = extractMechanisms(streamFeatures
555 .findChild("mechanisms"));
556 if (mechanisms.contains("PLAIN")) {
557 sendSaslAuthPlain();
558 } else if (mechanisms.contains("DIGEST-MD5")) {
559 sendSaslAuthDigestMd5();
560 }
561 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
562 + smVersion)
563 && streamId != null) {
564 ResumePacket resume = new ResumePacket(this.streamId,
565 stanzasReceived, smVersion);
566 this.tagWriter.writeStanzaAsync(resume);
567 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
568 sendBindRequest();
569 }
570 }
571
572 private boolean compressionAvailable() {
573 if (!this.streamFeatures.hasChild("compression",
574 "http://jabber.org/features/compress"))
575 return false;
576 if (!ZLibOutputStream.SUPPORTED)
577 return false;
578 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
579 return false;
580
581 Element compression = this.streamFeatures.findChild("compression",
582 "http://jabber.org/features/compress");
583 for (Element child : compression.getChildren()) {
584 if (!"method".equals(child.getName()))
585 continue;
586
587 if ("zlib".equalsIgnoreCase(child.getContent())) {
588 return true;
589 }
590 }
591 return false;
592 }
593
594 private List<String> extractMechanisms(Element stream) {
595 ArrayList<String> mechanisms = new ArrayList<String>(stream
596 .getChildren().size());
597 for (Element child : stream.getChildren()) {
598 mechanisms.add(child.getContent());
599 }
600 return mechanisms;
601 }
602
603 private void sendRegistryRequest() {
604 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
605 register.query("jabber:iq:register");
606 register.setTo(account.getServer());
607 sendIqPacket(register, new OnIqPacketReceived() {
608
609 @Override
610 public void onIqPacketReceived(Account account, IqPacket packet) {
611 Element instructions = packet.query().findChild("instructions");
612 if (packet.query().hasChild("username")
613 && (packet.query().hasChild("password"))) {
614 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
615 Element username = new Element("username")
616 .setContent(account.getUsername());
617 Element password = new Element("password")
618 .setContent(account.getPassword());
619 register.query("jabber:iq:register").addChild(username);
620 register.query().addChild(password);
621 sendIqPacket(register, new OnIqPacketReceived() {
622
623 @Override
624 public void onIqPacketReceived(Account account,
625 IqPacket packet) {
626 if (packet.getType() == IqPacket.TYPE_RESULT) {
627 account.setOption(Account.OPTION_REGISTER,
628 false);
629 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
630 } else if (packet.hasChild("error")
631 && (packet.findChild("error")
632 .hasChild("conflict"))) {
633 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
634 } else {
635 changeStatus(Account.STATUS_REGISTRATION_FAILED);
636 Log.d(LOGTAG, packet.toString());
637 }
638 disconnect(true);
639 }
640 });
641 } else {
642 changeStatus(Account.STATUS_REGISTRATION_FAILED);
643 disconnect(true);
644 Log.d(LOGTAG, account.getJid()
645 + ": could not register. instructions are"
646 + instructions.getContent());
647 }
648 }
649 });
650 }
651
652 private void sendBindRequest() throws IOException {
653 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
654 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
655 .addChild("resource").setContent(account.getResource());
656 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
657 @Override
658 public void onIqPacketReceived(Account account, IqPacket packet) {
659 String resource = packet.findChild("bind").findChild("jid")
660 .getContent().split("/")[1];
661 account.setResource(resource);
662 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
663 smVersion = 3;
664 EnablePacket enable = new EnablePacket(smVersion);
665 tagWriter.writeStanzaAsync(enable);
666 } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
667 smVersion = 2;
668 EnablePacket enable = new EnablePacket(smVersion);
669 tagWriter.writeStanzaAsync(enable);
670 }
671 sendServiceDiscoveryInfo(account.getServer());
672 sendServiceDiscoveryItems(account.getServer());
673 if (bindListener != null) {
674 bindListener.onBind(account);
675 }
676
677 changeStatus(Account.STATUS_ONLINE);
678 }
679 });
680 if (this.streamFeatures.hasChild("session")) {
681 Log.d(LOGTAG, account.getJid() + ": sending deprecated session");
682 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
683 startSession.addChild("session",
684 "urn:ietf:params:xml:ns:xmpp-session");
685 this.sendUnboundIqPacket(startSession, null);
686 }
687 }
688
689 private void sendServiceDiscoveryInfo(final String server) {
690 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
691 iq.setTo(server);
692 iq.query("http://jabber.org/protocol/disco#info");
693 this.sendIqPacket(iq, new OnIqPacketReceived() {
694
695 @Override
696 public void onIqPacketReceived(Account account, IqPacket packet) {
697 List<Element> elements = packet.query().getChildren();
698 List<String> features = new ArrayList<String>();
699 for (int i = 0; i < elements.size(); ++i) {
700 if (elements.get(i).getName().equals("feature")) {
701 features.add(elements.get(i).getAttribute("var"));
702 }
703 }
704 disco.put(server, features);
705
706 if (account.getServer().equals(server)) {
707 enableAdvancedStreamFeatures();
708 }
709 }
710 });
711 }
712
713 private void enableAdvancedStreamFeatures() {
714 if (hasFeaturesCarbon()) {
715 sendEnableCarbons();
716 }
717 }
718
719 private void sendServiceDiscoveryItems(final String server) {
720 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
721 iq.setTo(server);
722 iq.query("http://jabber.org/protocol/disco#items");
723 this.sendIqPacket(iq, new OnIqPacketReceived() {
724
725 @Override
726 public void onIqPacketReceived(Account account, IqPacket packet) {
727 List<Element> elements = packet.query().getChildren();
728 for (int i = 0; i < elements.size(); ++i) {
729 if (elements.get(i).getName().equals("item")) {
730 String jid = elements.get(i).getAttribute("jid");
731 sendServiceDiscoveryInfo(jid);
732 }
733 }
734 }
735 });
736 }
737
738 private void sendEnableCarbons() {
739 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
740 iq.addChild("enable", "urn:xmpp:carbons:2");
741 this.sendIqPacket(iq, new OnIqPacketReceived() {
742
743 @Override
744 public void onIqPacketReceived(Account account, IqPacket packet) {
745 if (!packet.hasChild("error")) {
746 Log.d(LOGTAG, account.getJid()
747 + ": successfully enabled carbons");
748 } else {
749 Log.d(LOGTAG, account.getJid()
750 + ": error enableing carbons " + packet.toString());
751 }
752 }
753 });
754 }
755
756 private void processStreamError(Tag currentTag) {
757 Log.d(LOGTAG, "processStreamError");
758 }
759
760 private void sendStartStream() throws IOException {
761 Tag stream = Tag.start("stream:stream");
762 stream.setAttribute("from", account.getJid());
763 stream.setAttribute("to", account.getServer());
764 stream.setAttribute("version", "1.0");
765 stream.setAttribute("xml:lang", "en");
766 stream.setAttribute("xmlns", "jabber:client");
767 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
768 tagWriter.writeTag(stream);
769 }
770
771 private String nextRandomId() {
772 return new BigInteger(50, mRandom).toString(32);
773 }
774
775 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
776 if (packet.getId() == null) {
777 String id = nextRandomId();
778 packet.setAttribute("id", id);
779 }
780 packet.setFrom(account.getFullJid());
781 this.sendPacket(packet, callback);
782 }
783
784 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
785 if (packet.getId() == null) {
786 String id = nextRandomId();
787 packet.setAttribute("id", id);
788 }
789 this.sendPacket(packet, callback);
790 }
791
792 public void sendMessagePacket(MessagePacket packet) {
793 this.sendPacket(packet, null);
794 }
795
796 public void sendMessagePacket(MessagePacket packet,
797 OnMessagePacketReceived callback) {
798 this.sendPacket(packet, callback);
799 }
800
801 public void sendPresencePacket(PresencePacket packet) {
802 this.sendPacket(packet, null);
803 }
804
805 public void sendPresencePacket(PresencePacket packet,
806 OnPresencePacketReceived callback) {
807 this.sendPacket(packet, callback);
808 }
809
810 private synchronized void sendPacket(final AbstractStanza packet,
811 PacketReceived callback) {
812 // TODO dont increment stanza count if packet = request packet or ack;
813 ++stanzasSent;
814 tagWriter.writeStanzaAsync(packet);
815 if (callback != null) {
816 if (packet.getId() == null) {
817 packet.setId(nextRandomId());
818 }
819 packetCallbacks.put(packet.getId(), callback);
820 }
821 }
822
823 public void sendPing() {
824 if (streamFeatures.hasChild("sm")) {
825 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
826 } else {
827 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
828 iq.setFrom(account.getFullJid());
829 iq.addChild("ping", "urn:xmpp:ping");
830 this.sendIqPacket(iq, null);
831 }
832 }
833
834 public void setOnMessagePacketReceivedListener(
835 OnMessagePacketReceived listener) {
836 this.messageListener = listener;
837 }
838
839 public void setOnUnregisteredIqPacketReceivedListener(
840 OnIqPacketReceived listener) {
841 this.unregisteredIqListener = listener;
842 }
843
844 public void setOnPresencePacketReceivedListener(
845 OnPresencePacketReceived listener) {
846 this.presenceListener = listener;
847 }
848
849 public void setOnJinglePacketReceivedListener(
850 OnJinglePacketReceived listener) {
851 this.jingleListener = listener;
852 }
853
854 public void setOnStatusChangedListener(OnStatusChanged listener) {
855 this.statusListener = listener;
856 }
857
858 public void setOnTLSExceptionReceivedListener(
859 OnTLSExceptionReceived listener) {
860 this.tlsListener = listener;
861 }
862
863 public void setOnBindListener(OnBindListener listener) {
864 this.bindListener = listener;
865 }
866
867 public void disconnect(boolean force) {
868 changeStatus(Account.STATUS_OFFLINE);
869 Log.d(LOGTAG, "disconnecting");
870 try {
871 if (force) {
872 socket.close();
873 return;
874 }
875 new Thread(new Runnable() {
876
877 @Override
878 public void run() {
879 if (tagWriter.isActive()) {
880 tagWriter.finish();
881 try {
882 while (!tagWriter.finished()) {
883 Log.d(LOGTAG, "not yet finished");
884 Thread.sleep(100);
885 }
886 tagWriter.writeTag(Tag.end("stream:stream"));
887 } catch (IOException e) {
888 Log.d(LOGTAG, "io exception during disconnect");
889 } catch (InterruptedException e) {
890 Log.d(LOGTAG, "interrupted");
891 }
892 }
893 }
894 }).start();
895 } catch (IOException e) {
896 Log.d(LOGTAG, "io exception during disconnect");
897 }
898 }
899
900 public boolean hasFeatureRosterManagment() {
901 if (this.streamFeatures == null) {
902 return false;
903 } else {
904 return this.streamFeatures.hasChild("ver");
905 }
906 }
907
908 public boolean hasFeatureStreamManagment() {
909 if (this.streamFeatures == null) {
910 return false;
911 } else {
912 return this.streamFeatures.hasChild("sm");
913 }
914 }
915
916 public boolean hasFeaturesCarbon() {
917 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
918 }
919
920 public boolean hasDiscoFeature(String server, String feature) {
921 if (!disco.containsKey(server)) {
922 return false;
923 }
924 return disco.get(server).contains(feature);
925 }
926
927 public String findDiscoItemByFeature(String feature) {
928 Iterator<Entry<String, List<String>>> it = this.disco.entrySet()
929 .iterator();
930 while (it.hasNext()) {
931 Entry<String, List<String>> pairs = it.next();
932 if (pairs.getValue().contains(feature)&&pairs.getValue().size()==1) {
933 return pairs.getKey();
934 }
935 it.remove();
936 }
937 return null;
938 }
939
940 public void r() {
941 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
942 }
943
944 public int getReceivedStanzas() {
945 return this.stanzasReceived;
946 }
947
948 public int getSentStanzas() {
949 return this.stanzasSent;
950 }
951
952 public String getMucServer() {
953 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
954 }
955
956 public int getTimeToNextAttempt() {
957 int interval = (int) (25 * Math.pow(1.5, attempt));
958 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
959 return interval - secondsSinceLast;
960 }
961
962 public int getAttempt() {
963 return this.attempt;
964 }
965}