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