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