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