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
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 Element compress = new Element("compress");
391 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
392 compress.addChild("method").setContent("zlib");
393 tagWriter.writeElement(compress);
394 }
395
396 private void switchOverToZLib(Tag currentTag) throws XmlPullParserException,
397 IOException, NoSuchAlgorithmException {
398 tagReader.readTag(); // read tag close
399
400 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter.getOutputStream()));
401 tagReader.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
402
403 sendStartStream();
404 Log.d(LOGTAG,account.getJid()+": compression enabled");
405 processStream(tagReader.readTag());
406 }
407
408 private void sendStartTLS() throws IOException {
409 Tag startTLS = Tag.empty("starttls");
410 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
411 tagWriter.writeTag(startTLS);
412 }
413
414 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
415 IOException {
416 Tag nextTag = tagReader.readTag(); // should be proceed end tag
417 try {
418 SSLContext sc = SSLContext.getInstance("TLS");
419 TrustManagerFactory tmf = TrustManagerFactory
420 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
421 // Initialise the TMF as you normally would, for example:
422 // tmf.in
423 try {
424 tmf.init((KeyStore) null);
425 } catch (KeyStoreException e1) {
426 // TODO Auto-generated catch block
427 e1.printStackTrace();
428 }
429
430 TrustManager[] trustManagers = tmf.getTrustManagers();
431 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
432
433 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
434
435 @Override
436 public void checkClientTrusted(X509Certificate[] chain,
437 String authType) throws CertificateException {
438 origTrustmanager.checkClientTrusted(chain, authType);
439 }
440
441 @Override
442 public void checkServerTrusted(X509Certificate[] chain,
443 String authType) throws CertificateException {
444 try {
445 origTrustmanager.checkServerTrusted(chain, authType);
446 } catch (CertificateException e) {
447 if (e.getCause() instanceof CertPathValidatorException) {
448 String sha;
449 try {
450 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
451 sha1.update(chain[0].getEncoded());
452 sha = CryptoHelper.bytesToHex(sha1.digest());
453 if (!sha.equals(account.getSSLFingerprint())) {
454 changeStatus(Account.STATUS_TLS_ERROR);
455 if (tlsListener!=null) {
456 tlsListener.onTLSExceptionReceived(sha,account);
457 }
458 throw new CertificateException();
459 }
460 } catch (NoSuchAlgorithmException e1) {
461 // TODO Auto-generated catch block
462 e1.printStackTrace();
463 }
464 } else {
465 throw new CertificateException();
466 }
467 }
468 }
469
470 @Override
471 public X509Certificate[] getAcceptedIssuers() {
472 return origTrustmanager.getAcceptedIssuers();
473 }
474
475 } };
476 sc.init(null, wrappedTrustManagers, null);
477 SSLSocketFactory factory = sc.getSocketFactory();
478 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
479 socket.getInetAddress().getHostAddress(), socket.getPort(),
480 true);
481 tagReader.setInputStream(sslSocket.getInputStream());
482 tagWriter.setOutputStream(sslSocket.getOutputStream());
483 sendStartStream();
484 Log.d(LOGTAG,account.getJid()+": TLS connection established");
485 processStream(tagReader.readTag());
486 sslSocket.close();
487 } catch (NoSuchAlgorithmException e1) {
488 // TODO Auto-generated catch block
489 e1.printStackTrace();
490 } catch (KeyManagementException e) {
491 // TODO Auto-generated catch block
492 e.printStackTrace();
493 }
494 }
495
496 private void sendSaslAuthPlain() throws IOException {
497 String saslString = CryptoHelper.saslPlain(account.getUsername(),
498 account.getPassword());
499 Element auth = new Element("auth");
500 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
501 auth.setAttribute("mechanism", "PLAIN");
502 auth.setContent(saslString);
503 tagWriter.writeElement(auth);
504 }
505
506 private void sendSaslAuthDigestMd5() throws IOException {
507 Element auth = new Element("auth");
508 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
509 auth.setAttribute("mechanism", "DIGEST-MD5");
510 tagWriter.writeElement(auth);
511 }
512
513 private void processStreamFeatures(Tag currentTag)
514 throws XmlPullParserException, IOException {
515 this.streamFeatures = tagReader.readElement(currentTag);
516 if (this.streamFeatures.hasChild("starttls")
517 && account.isOptionSet(Account.OPTION_USETLS)) {
518 sendStartTLS();
519 } else if (compressionAvailable()) {
520 sendCompressionZlib();
521 } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
522 sendRegistryRequest();
523 } else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
524 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
525 disconnect(true);
526 } else if (this.streamFeatures.hasChild("mechanisms")
527 && shouldAuthenticate) {
528 List<String> mechanisms = extractMechanisms( streamFeatures.findChild("mechanisms"));
529 if (mechanisms.contains("PLAIN")) {
530 sendSaslAuthPlain();
531 } else if (mechanisms.contains("DIGEST-MD5")) {
532 sendSaslAuthDigestMd5();
533 }
534 } else if (this.streamFeatures.hasChild("sm") && streamId != null) {
535 ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived);
536 this.tagWriter.writeStanzaAsync(resume);
537 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
538 sendBindRequest();
539 if (this.streamFeatures.hasChild("session")) {
540 Log.d(LOGTAG,account.getJid()+": sending deprecated session");
541 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
542 startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("")
543 this.sendIqPacket(startSession, null);
544 }
545 }
546 }
547
548 private boolean compressionAvailable() {
549 if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false;
550 if (!ZLibOutputStream.SUPPORTED) return false;
551 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false;
552
553 Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress");
554 for (Element child : compression.getChildren()) {
555 if (!"method".equals(child.getName())) continue;
556
557 if ("zlib".equalsIgnoreCase(child.getContent())) {
558 return true;
559 }
560 }
561 return false;
562 }
563
564 private List<String> extractMechanisms(Element stream) {
565 ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size());
566 for(Element child : stream.getChildren()) {
567 mechanisms.add(child.getContent());
568 }
569 return mechanisms;
570 }
571
572 private void sendRegistryRequest() {
573 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
574 register.query("jabber:iq:register");
575 register.setTo(account.getServer());
576 sendIqPacket(register, new OnIqPacketReceived() {
577
578 @Override
579 public void onIqPacketReceived(Account account, IqPacket packet) {
580 Element instructions = packet.query().findChild("instructions");
581 if (packet.query().hasChild("username")&&(packet.query().hasChild("password"))) {
582 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
583 Element username = new Element("username").setContent(account.getUsername());
584 Element password = new Element("password").setContent(account.getPassword());
585 register.query("jabber:iq:register").addChild(username);
586 register.query().addChild(password);
587 sendIqPacket(register, new OnIqPacketReceived() {
588
589 @Override
590 public void onIqPacketReceived(Account account, IqPacket packet) {
591 if (packet.getType()==IqPacket.TYPE_RESULT) {
592 account.setOption(Account.OPTION_REGISTER, false);
593 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
594 } else if (packet.hasChild("error")&&(packet.findChild("error").hasChild("conflict"))){
595 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
596 } else {
597 changeStatus(Account.STATUS_REGISTRATION_FAILED);
598 Log.d(LOGTAG,packet.toString());
599 }
600 disconnect(true);
601 }
602 });
603 } else {
604 changeStatus(Account.STATUS_REGISTRATION_FAILED);
605 disconnect(true);
606 Log.d(LOGTAG,account.getJid()+": could not register. instructions are"+instructions.getContent());
607 }
608 }
609 });
610 }
611
612 private void sendInitialPresence() {
613 PresencePacket packet = new PresencePacket();
614 packet.setAttribute("from", account.getFullJid());
615 if (account.getKeys().has("pgp_signature")) {
616 try {
617 String signature = account.getKeys().getString("pgp_signature");
618 packet.addChild("status").setContent("online");
619 packet.addChild("x","jabber:x:signed").setContent(signature);
620 } catch (JSONException e) {
621 //
622 }
623 }
624 this.sendPresencePacket(packet);
625 }
626
627 private void sendBindRequest() throws IOException {
628 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
629 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource());
630 this.sendIqPacket(iq, new OnIqPacketReceived() {
631 @Override
632 public void onIqPacketReceived(Account account, IqPacket packet) {
633 String resource = packet.findChild("bind").findChild("jid")
634 .getContent().split("/")[1];
635 account.setResource(resource);
636 if (streamFeatures.hasChild("sm")) {
637 String xmlns = streamFeatures.findChild("sm").getAttribute("xmlns");
638 EnablePacket enable = new EnablePacket(xmlns);
639 tagWriter.writeStanzaAsync(enable);
640 }
641 sendInitialPresence();
642 sendServiceDiscoveryInfo();
643 sendServiceDiscoveryItems();
644 if (bindListener !=null) {
645 bindListener.onBind(account);
646 }
647 changeStatus(Account.STATUS_ONLINE);
648 }
649 });
650 }
651
652 private void sendServiceDiscoveryInfo() {
653 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
654 iq.setTo(account.getServer());
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 for (int i = 0; i < elements.size(); ++i) {
662 if (elements.get(i).getName().equals("feature")) {
663 discoFeatures.add(elements.get(i).getAttribute(
664 "var"));
665 }
666 }
667 if (discoFeatures.contains("urn:xmpp:carbons:2")) {
668 sendEnableCarbons();
669 }
670 }
671 });
672 }
673 private void sendServiceDiscoveryItems() {
674 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
675 iq.setTo(account.getServer());
676 iq.query("http://jabber.org/protocol/disco#items");
677 this.sendIqPacket(iq, new OnIqPacketReceived() {
678
679 @Override
680 public void onIqPacketReceived(Account account, IqPacket packet) {
681 List<Element> elements = packet.query().getChildren();
682 for (int i = 0; i < elements.size(); ++i) {
683 if (elements.get(i).getName().equals("item")) {
684 discoItems.add(elements.get(i).getAttribute(
685 "jid"));
686 }
687 }
688 }
689 });
690 }
691
692 private void sendEnableCarbons() {
693 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
694 iq.addChild("enable","urn:xmpp:carbons:2");
695 this.sendIqPacket(iq, new OnIqPacketReceived() {
696
697 @Override
698 public void onIqPacketReceived(Account account, IqPacket packet) {
699 if (!packet.hasChild("error")) {
700 Log.d(LOGTAG, account.getJid()
701 + ": successfully enabled carbons");
702 } else {
703 Log.d(LOGTAG, account.getJid()
704 + ": error enableing carbons " + packet.toString());
705 }
706 }
707 });
708 }
709
710 private void processStreamError(Tag currentTag) {
711 Log.d(LOGTAG, "processStreamError");
712 }
713
714 private void sendStartStream() throws IOException {
715 Tag stream = Tag.start("stream:stream");
716 stream.setAttribute("from", account.getJid());
717 stream.setAttribute("to", account.getServer());
718 stream.setAttribute("version", "1.0");
719 stream.setAttribute("xml:lang", "en");
720 stream.setAttribute("xmlns", "jabber:client");
721 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
722 tagWriter.writeTag(stream);
723 }
724
725 private String nextRandomId() {
726 return new BigInteger(50, random).toString(32);
727 }
728
729 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
730 String id = nextRandomId();
731 packet.setAttribute("id", id);
732 packet.setFrom(account.getFullJid());
733 this.sendPacket(packet, callback);
734 }
735
736 public void sendMessagePacket(MessagePacket packet) {
737 this.sendPacket(packet, null);
738 }
739
740 public void sendMessagePacket(MessagePacket packet,
741 OnMessagePacketReceived callback) {
742 this.sendPacket(packet, callback);
743 }
744
745 public void sendPresencePacket(PresencePacket packet) {
746 this.sendPacket(packet, null);
747 }
748
749 public void sendPresencePacket(PresencePacket packet,
750 OnPresencePacketReceived callback) {
751 this.sendPacket(packet, callback);
752 }
753
754 private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
755 // TODO dont increment stanza count if packet = request packet or ack;
756 ++stanzasSent;
757 tagWriter.writeStanzaAsync(packet);
758 if (callback != null) {
759 if (packet.getId()==null) {
760 packet.setId(nextRandomId());
761 }
762 packetCallbacks.put(packet.getId(), callback);
763 }
764 }
765
766 public void sendPing() {
767 if (streamFeatures.hasChild("sm")) {
768 tagWriter.writeStanzaAsync(new RequestPacket());
769 } else {
770 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
771 iq.setFrom(account.getFullJid());
772 iq.addChild("ping","urn:xmpp:ping");
773 this.sendIqPacket(iq, null);
774 }
775 }
776
777 public void setOnMessagePacketReceivedListener(
778 OnMessagePacketReceived listener) {
779 this.messageListener = listener;
780 }
781
782 public void setOnUnregisteredIqPacketReceivedListener(
783 OnIqPacketReceived listener) {
784 this.unregisteredIqListener = listener;
785 }
786
787 public void setOnPresencePacketReceivedListener(
788 OnPresencePacketReceived listener) {
789 this.presenceListener = listener;
790 }
791
792 public void setOnJinglePacketReceivedListener(OnJinglePacketReceived listener) {
793 this.jingleListener = listener;
794 }
795
796 public void setOnStatusChangedListener(OnStatusChanged listener) {
797 this.statusListener = listener;
798 }
799
800 public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
801 this.tlsListener = listener;
802 }
803
804 public void setOnBindListener(OnBindListener listener) {
805 this.bindListener = listener;
806 }
807
808 public void disconnect(boolean force) {
809 changeStatus(Account.STATUS_OFFLINE);
810 Log.d(LOGTAG,"disconnecting");
811 try {
812 if (force) {
813 socket.close();
814 return;
815 }
816 if (tagWriter.isActive()) {
817 tagWriter.finish();
818 while(!tagWriter.finished()) {
819 //Log.d(LOGTAG,"not yet finished");
820 Thread.sleep(100);
821 }
822 tagWriter.writeTag(Tag.end("stream:stream"));
823 }
824 } catch (IOException e) {
825 Log.d(LOGTAG,"io exception during disconnect");
826 } catch (InterruptedException e) {
827 Log.d(LOGTAG,"interupted while waiting for disconnect");
828 }
829 }
830
831 public boolean hasFeatureRosterManagment() {
832 if (this.streamFeatures==null) {
833 return false;
834 } else {
835 return this.streamFeatures.hasChild("ver");
836 }
837 }
838
839 public boolean hasFeatureStreamManagment() {
840 if (this.streamFeatures==null) {
841 return false;
842 } else {
843 return this.streamFeatures.hasChild("sm");
844 }
845 }
846
847 public boolean hasFeaturesCarbon() {
848 return discoFeatures.contains("urn:xmpp:carbons:2");
849 }
850
851 public void r() {
852 this.tagWriter.writeStanzaAsync(new RequestPacket());
853 }
854
855 public int getReceivedStanzas() {
856 return this.stanzasReceived;
857 }
858
859 public int getSentStanzas() {
860 return this.stanzasSent;
861 }
862
863 public String getMucServer() {
864 for(int i = 0; i < discoItems.size(); ++i) {
865 if (discoItems.get(i).contains("conference.")) {
866 return discoItems.get(i);
867 } else if (discoItems.get(i).contains("conf.")) {
868 return discoItems.get(i);
869 } else if (discoItems.get(i).contains("muc.")) {
870 return discoItems.get(i);
871 }
872 }
873 return null;
874 }
875}