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