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