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