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