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.HashSet;
19import java.util.Hashtable;
20import java.util.List;
21
22import javax.net.ssl.SSLContext;
23import javax.net.ssl.SSLSocket;
24import javax.net.ssl.SSLSocketFactory;
25import javax.net.ssl.TrustManager;
26import javax.net.ssl.TrustManagerFactory;
27import javax.net.ssl.X509TrustManager;
28
29import org.json.JSONException;
30import org.xmlpull.v1.XmlPullParserException;
31
32import android.content.IntentSender.SendIntentException;
33import android.os.Bundle;
34import android.os.PowerManager;
35import android.util.Log;
36import eu.siacs.conversations.entities.Account;
37import eu.siacs.conversations.utils.CryptoHelper;
38import eu.siacs.conversations.utils.DNSHelper;
39import eu.siacs.conversations.xml.Element;
40import eu.siacs.conversations.xml.Tag;
41import eu.siacs.conversations.xml.TagWriter;
42import eu.siacs.conversations.xml.XmlReader;
43import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
44import eu.siacs.conversations.xmpp.stanzas.IqPacket;
45import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
46import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
47import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
48import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
49import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
50import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
51
52public class XmppConnection implements Runnable {
53
54 protected Account account;
55 private static final String LOGTAG = "xmppService";
56
57 private PowerManager.WakeLock wakeLock;
58
59 private SecureRandom random = new SecureRandom();
60
61 private Socket socket;
62 private XmlReader tagReader;
63 private TagWriter tagWriter;
64
65 private boolean shouldBind = true;
66 private boolean shouldAuthenticate = true;
67 private Element streamFeatures;
68 private HashSet<String> discoFeatures = new HashSet<String>();
69
70 private String streamId = null;
71
72 private int stanzasReceived = 0;
73 private int stanzasSent = 0;
74
75 private static final int PACKET_IQ = 0;
76 private static final int PACKET_MESSAGE = 1;
77 private static final int PACKET_PRESENCE = 2;
78
79 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
80 private OnPresencePacketReceived presenceListener = null;
81 private OnIqPacketReceived unregisteredIqListener = null;
82 private OnMessagePacketReceived messageListener = null;
83 private OnStatusChanged statusListener = null;
84 private OnTLSExceptionReceived tlsListener;
85
86 public XmppConnection(Account account, PowerManager pm) {
87 this.account = account;
88 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
89 "XmppConnection");
90 tagReader = new XmlReader(wakeLock);
91 tagWriter = new TagWriter();
92 }
93
94 protected void changeStatus(int nextStatus) {
95 account.setStatus(nextStatus);
96 if (statusListener != null) {
97 statusListener.onStatusChanged(account);
98 }
99 }
100
101 protected void connect() {
102 Log.d(LOGTAG, "connecting");
103 try {
104 tagReader = new XmlReader(wakeLock);
105 tagWriter = new TagWriter();
106 packetCallbacks.clear();
107 this.changeStatus(Account.STATUS_CONNECTING);
108 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
109 String srvRecordServer = namePort.getString("name");
110 int srvRecordPort = namePort.getInt("port");
111 if (srvRecordServer != null) {
112 Log.d(LOGTAG, account.getJid() + ": using values from dns "
113 + srvRecordServer + ":" + srvRecordPort);
114 socket = new Socket(srvRecordServer, srvRecordPort);
115 } else {
116 socket = new Socket(account.getServer(), 5222);
117 }
118 OutputStream out = socket.getOutputStream();
119 tagWriter.setOutputStream(out);
120 InputStream in = socket.getInputStream();
121 tagReader.setInputStream(in);
122 tagWriter.beginDocument();
123 sendStartStream();
124 Tag nextTag;
125 while ((nextTag = tagReader.readTag()) != null) {
126 if (nextTag.isStart("stream")) {
127 processStream(nextTag);
128 break;
129 } else {
130 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
131 return;
132 }
133 }
134 if (socket.isConnected()) {
135 socket.close();
136 }
137 } catch (UnknownHostException e) {
138 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
139 if (wakeLock.isHeld()) {
140 wakeLock.release();
141 }
142 return;
143 } catch (IOException e) {
144 if (account.getStatus() != Account.STATUS_TLS_ERROR) {
145 this.changeStatus(Account.STATUS_OFFLINE);
146 }
147 if (wakeLock.isHeld()) {
148 wakeLock.release();
149 }
150 return;
151 } catch (XmlPullParserException e) {
152 this.changeStatus(Account.STATUS_OFFLINE);
153 Log.d(LOGTAG, "xml exception " + e.getMessage());
154 if (wakeLock.isHeld()) {
155 wakeLock.release();
156 }
157 return;
158 }
159
160 }
161
162 @Override
163 public void run() {
164 connect();
165 Log.d(LOGTAG, "end run");
166 }
167
168 private void processStream(Tag currentTag) throws XmlPullParserException,
169 IOException {
170 Tag nextTag = tagReader.readTag();
171 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
172 if (nextTag.isStart("error")) {
173 processStreamError(nextTag);
174 } else if (nextTag.isStart("features")) {
175 processStreamFeatures(nextTag);
176 if ((streamFeatures.getChildren().size() == 1)
177 && (streamFeatures.hasChild("starttls"))
178 && (!account.isOptionSet(Account.OPTION_USETLS))) {
179 changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
180 }
181 } else if (nextTag.isStart("proceed")) {
182 switchOverToTls(nextTag);
183 } else if (nextTag.isStart("success")) {
184 Log.d(LOGTAG, account.getJid()
185 + ": logged in");
186 tagReader.readTag();
187 tagReader.reset();
188 sendStartStream();
189 processStream(tagReader.readTag());
190 break;
191 } else if (nextTag.isStart("failure")) {
192 Element failure = tagReader.readElement(nextTag);
193 changeStatus(Account.STATUS_UNAUTHORIZED);
194 } else if (nextTag.isStart("enabled")) {
195 this.stanzasSent = 0;
196 Element enabled = tagReader.readElement(nextTag);
197 if ("true".equals(enabled.getAttribute("resume"))) {
198 this.streamId = enabled.getAttribute("id");
199 Log.d(LOGTAG,account.getJid()+": stream managment enabled (resumable)");
200 } else {
201 Log.d(LOGTAG,account.getJid()+": stream managment enabled");
202 }
203 this.stanzasReceived = 0;
204 RequestPacket r = new RequestPacket();
205 tagWriter.writeStanzaAsync(r);
206 } else if (nextTag.isStart("resumed")) {
207 tagReader.readElement(nextTag);
208 changeStatus(Account.STATUS_ONLINE);
209 Log.d(LOGTAG,account.getJid()+": session resumed");
210 } else if (nextTag.isStart("r")) {
211 tagReader.readElement(nextTag);
212 AckPacket ack = new AckPacket(this.stanzasReceived);
213 //Log.d(LOGTAG,ack.toString());
214 tagWriter.writeStanzaAsync(ack);
215 } else if (nextTag.isStart("a")) {
216 Element ack = tagReader.readElement(nextTag);
217 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
218 if (serverSequence>this.stanzasSent) {
219 this.stanzasSent = serverSequence;
220 }
221 //Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")");
222 } else if (nextTag.isStart("iq")) {
223 processIq(nextTag);
224 } else if (nextTag.isStart("message")) {
225 processMessage(nextTag);
226 } else if (nextTag.isStart("presence")) {
227 processPresence(nextTag);
228 } else {
229 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
230 + " as child of " + currentTag.getName());
231 }
232 nextTag = tagReader.readTag();
233 }
234 if (account.getStatus() == Account.STATUS_ONLINE) {
235 account.setStatus(Account.STATUS_OFFLINE);
236 if (statusListener != null) {
237 statusListener.onStatusChanged(account);
238 }
239 }
240 }
241
242 private Element processPacket(Tag currentTag, int packetType)
243 throws XmlPullParserException, IOException {
244 Element element;
245 switch (packetType) {
246 case PACKET_IQ:
247 element = new IqPacket();
248 break;
249 case PACKET_MESSAGE:
250 element = new MessagePacket();
251 break;
252 case PACKET_PRESENCE:
253 element = new PresencePacket();
254 break;
255 default:
256 return null;
257 }
258 element.setAttributes(currentTag.getAttributes());
259 Tag nextTag = tagReader.readTag();
260 while (!nextTag.isEnd(element.getName())) {
261 if (!nextTag.isNo()) {
262 Element child = tagReader.readElement(nextTag);
263 element.addChild(child);
264 }
265 nextTag = tagReader.readTag();
266 }
267 ++stanzasReceived;
268 return element;
269 }
270
271 private void processIq(Tag currentTag) throws XmlPullParserException,
272 IOException {
273 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
274 if (packetCallbacks.containsKey(packet.getId())) {
275 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
276 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
277 .onIqPacketReceived(account, packet);
278 }
279
280 packetCallbacks.remove(packet.getId());
281 } else if (this.unregisteredIqListener != null) {
282 this.unregisteredIqListener.onIqPacketReceived(account, packet);
283 }
284 }
285
286 private void processMessage(Tag currentTag) throws XmlPullParserException,
287 IOException {
288 MessagePacket packet = (MessagePacket) processPacket(currentTag,
289 PACKET_MESSAGE);
290 String id = packet.getAttribute("id");
291 if ((id != null) && (packetCallbacks.containsKey(id))) {
292 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
293 ((OnMessagePacketReceived) packetCallbacks.get(id))
294 .onMessagePacketReceived(account, packet);
295 }
296 packetCallbacks.remove(id);
297 } else if (this.messageListener != null) {
298 this.messageListener.onMessagePacketReceived(account, packet);
299 }
300 }
301
302 private void processPresence(Tag currentTag) throws XmlPullParserException,
303 IOException {
304 PresencePacket packet = (PresencePacket) processPacket(currentTag,
305 PACKET_PRESENCE);
306 String id = packet.getAttribute("id");
307 if ((id != null) && (packetCallbacks.containsKey(id))) {
308 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
309 ((OnPresencePacketReceived) packetCallbacks.get(id))
310 .onPresencePacketReceived(account, packet);
311 }
312 packetCallbacks.remove(id);
313 } else if (this.presenceListener != null) {
314 this.presenceListener.onPresencePacketReceived(account, packet);
315 }
316 }
317
318 private void sendStartTLS() throws IOException {
319 Tag startTLS = Tag.empty("starttls");
320 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
321 tagWriter.writeTag(startTLS);
322 }
323
324 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
325 IOException {
326 Tag nextTag = tagReader.readTag(); // should be proceed end tag
327 try {
328 SSLContext sc = SSLContext.getInstance("TLS");
329 TrustManagerFactory tmf = TrustManagerFactory
330 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
331 // Initialise the TMF as you normally would, for example:
332 // tmf.in
333 try {
334 tmf.init((KeyStore) null);
335 } catch (KeyStoreException e1) {
336 // TODO Auto-generated catch block
337 e1.printStackTrace();
338 }
339
340 TrustManager[] trustManagers = tmf.getTrustManagers();
341 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
342
343 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
344
345 @Override
346 public void checkClientTrusted(X509Certificate[] chain,
347 String authType) throws CertificateException {
348 origTrustmanager.checkClientTrusted(chain, authType);
349 }
350
351 @Override
352 public void checkServerTrusted(X509Certificate[] chain,
353 String authType) throws CertificateException {
354 try {
355 origTrustmanager.checkServerTrusted(chain, authType);
356 } catch (CertificateException e) {
357 if (e.getCause() instanceof CertPathValidatorException) {
358 String sha;
359 try {
360 MessageDigest sha1 = MessageDigest.getInstance("SHA1");
361 sha1.update(chain[0].getEncoded());
362 sha = CryptoHelper.bytesToHex(sha1.digest());
363 if (!sha.equals(account.getSSLFingerprint())) {
364 changeStatus(Account.STATUS_TLS_ERROR);
365 if (tlsListener!=null) {
366 tlsListener.onTLSExceptionReceived(sha,account);
367 }
368 throw new CertificateException();
369 }
370 } catch (NoSuchAlgorithmException e1) {
371 // TODO Auto-generated catch block
372 e1.printStackTrace();
373 }
374 } else {
375 throw new CertificateException();
376 }
377 }
378 }
379
380 @Override
381 public X509Certificate[] getAcceptedIssuers() {
382 return origTrustmanager.getAcceptedIssuers();
383 }
384
385 } };
386 sc.init(null, wrappedTrustManagers, null);
387 SSLSocketFactory factory = sc.getSocketFactory();
388 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
389 socket.getInetAddress().getHostAddress(), socket.getPort(),
390 true);
391 tagReader.setInputStream(sslSocket.getInputStream());
392 tagWriter.setOutputStream(sslSocket.getOutputStream());
393 sendStartStream();
394 Log.d(LOGTAG,account.getJid()+": TLS connection established");
395 processStream(tagReader.readTag());
396 sslSocket.close();
397 } catch (NoSuchAlgorithmException e1) {
398 // TODO Auto-generated catch block
399 e1.printStackTrace();
400 } catch (KeyManagementException e) {
401 // TODO Auto-generated catch block
402 e.printStackTrace();
403 }
404 }
405
406 private void sendSaslAuth() throws IOException, XmlPullParserException {
407 String saslString = CryptoHelper.saslPlain(account.getUsername(),
408 account.getPassword());
409 Element auth = new Element("auth");
410 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
411 auth.setAttribute("mechanism", "PLAIN");
412 auth.setContent(saslString);
413 tagWriter.writeElement(auth);
414 }
415
416 private void processStreamFeatures(Tag currentTag)
417 throws XmlPullParserException, IOException {
418 this.streamFeatures = tagReader.readElement(currentTag);
419 if (this.streamFeatures.hasChild("starttls")
420 && account.isOptionSet(Account.OPTION_USETLS)) {
421 sendStartTLS();
422 } else if (this.streamFeatures.hasChild("mechanisms")
423 && shouldAuthenticate) {
424 sendSaslAuth();
425 } else if (this.streamFeatures.hasChild("sm") && streamId != null) {
426 Log.d(LOGTAG,"found old stream id. trying to remuse");
427 ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived);
428 this.tagWriter.writeStanzaAsync(resume);
429 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
430 sendBindRequest();
431 if (this.streamFeatures.hasChild("session")) {
432 Log.d(LOGTAG,"sending session");
433 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
434 Element session = new Element("session");
435 session.setAttribute("xmlns",
436 "urn:ietf:params:xml:ns:xmpp-session");
437 session.setContent("");
438 startSession.addChild(session);
439 this.sendIqPacket(startSession, null);
440 }
441 }
442 }
443
444 private void sendInitialPresence() {
445 PresencePacket packet = new PresencePacket();
446 packet.setAttribute("from", account.getFullJid());
447 if (account.getKeys().has("pgp_signature")) {
448 try {
449 String signature = account.getKeys().getString("pgp_signature");
450 Element status = new Element("status");
451 status.setContent("online");
452 packet.addChild(status);
453 Element x = new Element("x");
454 x.setAttribute("xmlns", "jabber:x:signed");
455 x.setContent(signature);
456 packet.addChild(x);
457 } catch (JSONException e) {
458 //
459 }
460 }
461 this.sendPresencePacket(packet);
462 }
463
464 private void sendBindRequest() throws IOException {
465 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
466 Element bind = new Element("bind");
467 bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
468 Element resource = new Element("resource");
469 resource.setContent("Conversations");
470 bind.addChild(resource);
471 iq.addChild(bind);
472 this.sendIqPacket(iq, new OnIqPacketReceived() {
473 @Override
474 public void onIqPacketReceived(Account account, IqPacket packet) {
475 String resource = packet.findChild("bind").findChild("jid")
476 .getContent().split("/")[1];
477 account.setResource(resource);
478 account.setStatus(Account.STATUS_ONLINE);
479 if (streamFeatures.hasChild("sm")) {
480 EnablePacket enable = new EnablePacket();
481 tagWriter.writeStanzaAsync(enable);
482 }
483 sendInitialPresence();
484 sendServiceDiscovery();
485 if (statusListener != null) {
486 statusListener.onStatusChanged(account);
487 }
488 }
489 });
490 }
491
492 private void sendServiceDiscovery() {
493 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
494 iq.setAttribute("to", account.getServer());
495 Element query = new Element("query");
496 query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
497 iq.addChild(query);
498 this.sendIqPacket(iq, new OnIqPacketReceived() {
499
500 @Override
501 public void onIqPacketReceived(Account account, IqPacket packet) {
502 if (packet.hasChild("query")) {
503 List<Element> elements = packet.findChild("query")
504 .getChildren();
505 for (int i = 0; i < elements.size(); ++i) {
506 if (elements.get(i).getName().equals("feature")) {
507 discoFeatures.add(elements.get(i).getAttribute(
508 "var"));
509 }
510 }
511 }
512 if (discoFeatures.contains("urn:xmpp:carbons:2")) {
513 sendEnableCarbons();
514 }
515 }
516 });
517 }
518
519 private void sendEnableCarbons() {
520 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
521 Element enable = new Element("enable");
522 enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
523 iq.addChild(enable);
524 this.sendIqPacket(iq, new OnIqPacketReceived() {
525
526 @Override
527 public void onIqPacketReceived(Account account, IqPacket packet) {
528 if (!packet.hasChild("error")) {
529 Log.d(LOGTAG, account.getJid()
530 + ": successfully enabled carbons");
531 } else {
532 Log.d(LOGTAG, account.getJid()
533 + ": error enableing carbons " + packet.toString());
534 }
535 }
536 });
537 }
538
539 private void processStreamError(Tag currentTag) {
540 Log.d(LOGTAG, "processStreamError");
541 }
542
543 private void sendStartStream() throws IOException {
544 Tag stream = Tag.start("stream:stream");
545 stream.setAttribute("from", account.getJid());
546 stream.setAttribute("to", account.getServer());
547 stream.setAttribute("version", "1.0");
548 stream.setAttribute("xml:lang", "en");
549 stream.setAttribute("xmlns", "jabber:client");
550 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
551 tagWriter.writeTag(stream);
552 }
553
554 private String nextRandomId() {
555 return new BigInteger(50, random).toString(32);
556 }
557
558 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
559 String id = nextRandomId();
560 packet.setAttribute("id", id);
561 this.sendPacket(packet, callback);
562 }
563
564 public void sendMessagePacket(MessagePacket packet) {
565 this.sendPacket(packet, null);
566 }
567
568 public void sendMessagePacket(MessagePacket packet,
569 OnMessagePacketReceived callback) {
570 this.sendPacket(packet, callback);
571 }
572
573 public void sendPresencePacket(PresencePacket packet) {
574 this.sendPacket(packet, null);
575 }
576
577 public void sendPresencePacket(PresencePacket packet,
578 OnPresencePacketReceived callback) {
579 this.sendPacket(packet, callback);
580 }
581
582 private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
583 ++stanzasSent;
584 tagWriter.writeStanzaAsync(packet);
585 if (callback != null) {
586 if (packet.getId()==null) {
587 packet.setId(nextRandomId());
588 }
589 packetCallbacks.put(packet.getId(), callback);
590 }
591 }
592
593 public void setOnMessagePacketReceivedListener(
594 OnMessagePacketReceived listener) {
595 this.messageListener = listener;
596 }
597
598 public void setOnUnregisteredIqPacketReceivedListener(
599 OnIqPacketReceived listener) {
600 this.unregisteredIqListener = listener;
601 }
602
603 public void setOnPresencePacketReceivedListener(
604 OnPresencePacketReceived listener) {
605 this.presenceListener = listener;
606 }
607
608 public void setOnStatusChangedListener(OnStatusChanged listener) {
609 this.statusListener = listener;
610 }
611
612 public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
613 this.tlsListener = listener;
614 }
615
616 public void disconnect(boolean force) {
617 Log.d(LOGTAG,"disconnecting");
618 try {
619 if (force) {
620 socket.close();
621 }
622 tagWriter.finish();
623 while(!tagWriter.finished()) {
624 Log.d(LOGTAG,"not yet finished");
625 Thread.sleep(100);
626 }
627 tagWriter.writeTag(Tag.end("stream:stream"));
628 } catch (IOException e) {
629 Log.d(LOGTAG,"io exception during disconnect");
630 } catch (InterruptedException e) {
631 Log.d(LOGTAG,"interupted while waiting for disconnect");
632 }
633 }
634
635 public boolean hasFeatureRosterManagment() {
636 if (this.streamFeatures==null) {
637 return false;
638 } else {
639 return this.streamFeatures.hasChild("ver");
640 }
641 }
642
643 public void r() {
644 this.tagWriter.writeStanzaAsync(new RequestPacket());
645 }
646}