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