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
399 Log.d(LOGTAG,account.getJid()+": Starting zlib compressed stream");
400
401 tagReader.readTag(); // read tag close
402
403 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter.getOutputStream()));
404 tagReader.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
405
406 sendStartStream();
407 Log.d(LOGTAG,account.getJid()+": compression enabled");
408 processStream(tagReader.readTag());
409 }
410
411 private void sendStartTLS() throws IOException {
412 Tag startTLS = Tag.empty("starttls");
413 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
414 tagWriter.writeTag(startTLS);
415 }
416
417 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
418 IOException {
419 Tag nextTag = tagReader.readTag(); // should be proceed end tag
420 try {
421 SSLContext sc = SSLContext.getInstance("TLS");
422 TrustManagerFactory tmf = TrustManagerFactory
423 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
424 // Initialise the TMF as you normally would, for example:
425 // tmf.in
426 try {
427 tmf.init((KeyStore) null);
428 } catch (KeyStoreException e1) {
429 // TODO Auto-generated catch block
430 e1.printStackTrace();
431 }
432
433 TrustManager[] trustManagers = tmf.getTrustManagers();
434 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
435
436 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
437
438 @Override
439 public void checkClientTrusted(X509Certificate[] chain,
440 String authType) throws CertificateException {
441 origTrustmanager.checkClientTrusted(chain, authType);
442 }
443
444 @Override
445 public void checkServerTrusted(X509Certificate[] chain,
446 String authType) throws CertificateException {
447 try {
448 origTrustmanager.checkServerTrusted(chain, authType);
449 } catch (CertificateException e) {
450 if (e.getCause() instanceof CertPathValidatorException) {
451 String sha;
452 try {
453 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
454 sha1.update(chain[0].getEncoded());
455 sha = CryptoHelper.bytesToHex(sha1.digest());
456 if (!sha.equals(account.getSSLFingerprint())) {
457 changeStatus(Account.STATUS_TLS_ERROR);
458 if (tlsListener!=null) {
459 tlsListener.onTLSExceptionReceived(sha,account);
460 }
461 throw new CertificateException();
462 }
463 } catch (NoSuchAlgorithmException e1) {
464 // TODO Auto-generated catch block
465 e1.printStackTrace();
466 }
467 } else {
468 throw new CertificateException();
469 }
470 }
471 }
472
473 @Override
474 public X509Certificate[] getAcceptedIssuers() {
475 return origTrustmanager.getAcceptedIssuers();
476 }
477
478 } };
479 sc.init(null, wrappedTrustManagers, null);
480 SSLSocketFactory factory = sc.getSocketFactory();
481 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
482 socket.getInetAddress().getHostAddress(), socket.getPort(),
483 true);
484 tagReader.setInputStream(sslSocket.getInputStream());
485 tagWriter.setOutputStream(sslSocket.getOutputStream());
486 sendStartStream();
487 Log.d(LOGTAG,account.getJid()+": TLS connection established");
488 processStream(tagReader.readTag());
489 sslSocket.close();
490 } catch (NoSuchAlgorithmException e1) {
491 // TODO Auto-generated catch block
492 e1.printStackTrace();
493 } catch (KeyManagementException e) {
494 // TODO Auto-generated catch block
495 e.printStackTrace();
496 }
497 }
498
499 private void sendSaslAuthPlain() throws IOException {
500 String saslString = CryptoHelper.saslPlain(account.getUsername(),
501 account.getPassword());
502 Element auth = new Element("auth");
503 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
504 auth.setAttribute("mechanism", "PLAIN");
505 auth.setContent(saslString);
506 tagWriter.writeElement(auth);
507 }
508
509 private void sendSaslAuthDigestMd5() throws IOException {
510 Element auth = new Element("auth");
511 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
512 auth.setAttribute("mechanism", "DIGEST-MD5");
513 tagWriter.writeElement(auth);
514 }
515
516 private void processStreamFeatures(Tag currentTag)
517 throws XmlPullParserException, IOException {
518 this.streamFeatures = tagReader.readElement(currentTag);
519 if (this.streamFeatures.hasChild("starttls")
520 && account.isOptionSet(Account.OPTION_USETLS)) {
521 sendStartTLS();
522 } else if (compressionAvailable()) {
523 sendCompressionZlib();
524 } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
525 sendRegistryRequest();
526 } else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
527 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
528 disconnect(true);
529 } else if (this.streamFeatures.hasChild("mechanisms")
530 && shouldAuthenticate) {
531 List<String> mechanisms = extractMechanisms( streamFeatures.findChild("mechanisms"));
532 if (mechanisms.contains("PLAIN")) {
533 sendSaslAuthPlain();
534 } else if (mechanisms.contains("DIGEST-MD5")) {
535 sendSaslAuthDigestMd5();
536 }
537 } else if (this.streamFeatures.hasChild("sm") && streamId != null) {
538 Log.d(LOGTAG,"found old stream id. trying to remuse");
539 ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived);
540 this.tagWriter.writeStanzaAsync(resume);
541 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
542 sendBindRequest();
543 if (this.streamFeatures.hasChild("session")) {
544 Log.d(LOGTAG,"sending session");
545 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
546 startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("")
547 this.sendIqPacket(startSession, null);
548 }
549 }
550 }
551
552 private boolean compressionAvailable() {
553 if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false;
554 if (!ZLibOutputStream.SUPPORTED) return false;
555 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false;
556
557 Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress");
558 for (Element child : compression.getChildren()) {
559 if (!"method".equals(child.getName())) continue;
560
561 if ("zlib".equalsIgnoreCase(child.getContent())) {
562 Log.d(LOGTAG, account.getJid() + ": compression available");
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 sendInitialPresence() {
618 PresencePacket packet = new PresencePacket();
619 packet.setAttribute("from", account.getFullJid());
620 if (account.getKeys().has("pgp_signature")) {
621 try {
622 String signature = account.getKeys().getString("pgp_signature");
623 packet.addChild("status").setContent("online");
624 packet.addChild("x","jabber:x:signed").setContent(signature);
625 } catch (JSONException e) {
626 //
627 }
628 }
629 this.sendPresencePacket(packet);
630 }
631
632 private void sendBindRequest() throws IOException {
633 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
634 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource());
635 this.sendIqPacket(iq, new OnIqPacketReceived() {
636 @Override
637 public void onIqPacketReceived(Account account, IqPacket packet) {
638 String resource = packet.findChild("bind").findChild("jid")
639 .getContent().split("/")[1];
640 account.setResource(resource);
641 if (streamFeatures.hasChild("sm")) {
642 String xmlns = streamFeatures.findChild("sm").getAttribute("xmlns");
643 EnablePacket enable = new EnablePacket(xmlns);
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());
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());
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}