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.NoSuchAlgorithmException;
13import java.security.SecureRandom;
14import java.security.cert.CertPathValidatorException;
15import java.security.cert.CertificateException;
16import java.security.cert.CertificateExpiredException;
17import java.security.cert.X509Certificate;
18import java.util.HashSet;
19import java.util.Hashtable;
20import java.util.List;
21
22import javax.net.ssl.ManagerFactoryParameters;
23import javax.net.ssl.SSLContext;
24import javax.net.ssl.SSLSocket;
25import javax.net.ssl.SSLSocketFactory;
26import javax.net.ssl.TrustManager;
27import javax.net.ssl.TrustManagerFactory;
28import javax.net.ssl.X509TrustManager;
29
30import org.xmlpull.v1.XmlPullParserException;
31
32import android.os.Bundle;
33import android.os.PowerManager;
34import android.util.Log;
35import eu.siacs.conversations.entities.Account;
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 isTlsEncrypted = false;
57 private boolean isAuthenticated = false;
58 // private boolean shouldUseTLS = false;
59 private boolean shouldConnect = true;
60 private boolean shouldBind = true;
61 private boolean shouldAuthenticate = true;
62 private Element streamFeatures;
63 private HashSet<String> discoFeatures = new HashSet<String>();
64
65 private static final int PACKET_IQ = 0;
66 private static final int PACKET_MESSAGE = 1;
67 private static final int PACKET_PRESENCE = 2;
68
69 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
70 private OnPresencePacketReceived presenceListener = null;
71 private OnIqPacketReceived unregisteredIqListener = null;
72 private OnMessagePacketReceived messageListener = null;
73 private OnStatusChanged statusListener = null;
74
75 public XmppConnection(Account account, PowerManager pm) {
76 this.account = account;
77 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
78 "XmppConnection");
79 tagReader = new XmlReader(wakeLock);
80 tagWriter = new TagWriter();
81 }
82
83 protected void changeStatus(int nextStatus) {
84 account.setStatus(nextStatus);
85 if (statusListener != null) {
86 statusListener.onStatusChanged(account);
87 }
88 }
89
90 protected void connect() {
91 Log.d(LOGTAG, "connecting");
92 try {
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 this.changeStatus(Account.STATUS_OFFLINE);
131 if (wakeLock.isHeld()) {
132 wakeLock.release();
133 }
134 return;
135 } catch (XmlPullParserException e) {
136 this.changeStatus(Account.STATUS_OFFLINE);
137 Log.d(LOGTAG, "xml exception " + e.getMessage());
138 if (wakeLock.isHeld()) {
139 wakeLock.release();
140 }
141 return;
142 }
143
144 }
145
146 @Override
147 public void run() {
148 connect();
149 Log.d(LOGTAG, "end run");
150 }
151
152 private void processStream(Tag currentTag) throws XmlPullParserException,
153 IOException {
154 Tag nextTag = tagReader.readTag();
155 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
156 if (nextTag.isStart("error")) {
157 processStreamError(nextTag);
158 } else if (nextTag.isStart("features")) {
159 processStreamFeatures(nextTag);
160 if ((streamFeatures.getChildren().size() == 1)
161 && (streamFeatures.hasChild("starttls"))
162 && (!account.isOptionSet(Account.OPTION_USETLS))) {
163 changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
164 }
165 } else if (nextTag.isStart("proceed")) {
166 switchOverToTls(nextTag);
167 } else if (nextTag.isStart("success")) {
168 isAuthenticated = true;
169 Log.d(LOGTAG, account.getJid()
170 + ": read success tag in stream. reset again");
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 Log.d(LOGTAG, account.getJid() + ": sending starttls");
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 Log.d(LOGTAG, account.getJid() + ": now switch to ssl");
285 try {
286 SSLContext sc = SSLContext.getInstance("TLS");
287 TrustManagerFactory tmf = TrustManagerFactory
288 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
289 // Initialise the TMF as you normally would, for example:
290 // tmf.in
291 try {
292 tmf.init((KeyStore) null);
293 } catch (KeyStoreException e1) {
294 // TODO Auto-generated catch block
295 e1.printStackTrace();
296 }
297
298 TrustManager[] trustManagers = tmf.getTrustManagers();
299 final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
300
301 TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
302
303 @Override
304 public void checkClientTrusted(X509Certificate[] chain,
305 String authType) throws CertificateException {
306 origTrustmanager.checkClientTrusted(chain, authType);
307 }
308
309 @Override
310 public void checkServerTrusted(X509Certificate[] chain,
311 String authType) throws CertificateException {
312 try {
313 origTrustmanager.checkServerTrusted(chain, authType);
314 } catch (CertificateException e) {
315 Log.d(LOGTAG,"cert exeption");
316 }
317 }
318
319 @Override
320 public X509Certificate[] getAcceptedIssuers() {
321 return origTrustmanager.getAcceptedIssuers();
322 }
323
324 } };
325 sc.init(null, wrappedTrustManagers, null);
326 SSLSocketFactory factory = sc.getSocketFactory();
327 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
328 socket.getInetAddress().getHostAddress(), socket.getPort(),
329 true);
330 tagReader.setInputStream(sslSocket.getInputStream());
331 Log.d(LOGTAG, "reset inputstream");
332 tagWriter.setOutputStream(sslSocket.getOutputStream());
333 Log.d(LOGTAG, "switch over seemed to work");
334 isTlsEncrypted = true;
335 sendStartStream();
336 processStream(tagReader.readTag());
337 sslSocket.close();
338 } catch (NoSuchAlgorithmException e1) {
339 // TODO Auto-generated catch block
340 e1.printStackTrace();
341 } catch (KeyManagementException e) {
342 // TODO Auto-generated catch block
343 e.printStackTrace();
344 }
345 }
346
347 private void sendSaslAuth() throws IOException, XmlPullParserException {
348 String saslString = SASL.plain(account.getUsername(),
349 account.getPassword());
350 Element auth = new Element("auth");
351 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
352 auth.setAttribute("mechanism", "PLAIN");
353 auth.setContent(saslString);
354 Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString());
355 tagWriter.writeElement(auth);
356 }
357
358 private void processStreamFeatures(Tag currentTag)
359 throws XmlPullParserException, IOException {
360 this.streamFeatures = tagReader.readElement(currentTag);
361 Log.d(LOGTAG, account.getJid() + ": process stream features "
362 + streamFeatures);
363 if (this.streamFeatures.hasChild("starttls")
364 && account.isOptionSet(Account.OPTION_USETLS)) {
365 sendStartTLS();
366 } else if (this.streamFeatures.hasChild("mechanisms")
367 && shouldAuthenticate) {
368 sendSaslAuth();
369 }
370 if (this.streamFeatures.hasChild("bind") && shouldBind) {
371 sendBindRequest();
372 if (this.streamFeatures.hasChild("session")) {
373 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
374 Element session = new Element("session");
375 session.setAttribute("xmlns",
376 "urn:ietf:params:xml:ns:xmpp-session");
377 session.setContent("");
378 startSession.addChild(session);
379 sendIqPacket(startSession, null);
380 tagWriter.writeElement(startSession);
381 }
382 Element presence = new Element("presence");
383
384 tagWriter.writeElement(presence);
385 }
386 }
387
388 private void sendBindRequest() throws IOException {
389 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
390 Element bind = new Element("bind");
391 bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
392 iq.addChild(bind);
393 this.sendIqPacket(iq, new OnIqPacketReceived() {
394 @Override
395 public void onIqPacketReceived(Account account, IqPacket packet) {
396 String resource = packet.findChild("bind").findChild("jid")
397 .getContent().split("/")[1];
398 account.setResource(resource);
399 account.setStatus(Account.STATUS_ONLINE);
400 if (statusListener != null) {
401 statusListener.onStatusChanged(account);
402 }
403 sendServiceDiscovery();
404 }
405 });
406 }
407
408 private void sendServiceDiscovery() {
409 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
410 iq.setAttribute("to", account.getServer());
411 Element query = new Element("query");
412 query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
413 iq.addChild(query);
414 this.sendIqPacket(iq, new OnIqPacketReceived() {
415
416 @Override
417 public void onIqPacketReceived(Account account, IqPacket packet) {
418 if (packet.hasChild("query")) {
419 List<Element> elements = packet.findChild("query")
420 .getChildren();
421 for (int i = 0; i < elements.size(); ++i) {
422 if (elements.get(i).getName().equals("feature")) {
423 discoFeatures.add(elements.get(i).getAttribute(
424 "var"));
425 }
426 }
427 }
428 if (discoFeatures.contains("urn:xmpp:carbons:2")) {
429 sendEnableCarbons();
430 }
431 }
432 });
433 }
434
435 private void sendEnableCarbons() {
436 Log.d(LOGTAG, account.getJid() + ": enable carbons");
437 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
438 Element enable = new Element("enable");
439 enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
440 iq.addChild(enable);
441 this.sendIqPacket(iq, new OnIqPacketReceived() {
442
443 @Override
444 public void onIqPacketReceived(Account account, IqPacket packet) {
445 if (!packet.hasChild("error")) {
446 Log.d(LOGTAG, account.getJid()
447 + ": successfully enabled carbons");
448 } else {
449 Log.d(LOGTAG, account.getJid()
450 + ": error enableing carbons " + packet.toString());
451 }
452 }
453 });
454 }
455
456 private void processStreamError(Tag currentTag) {
457 Log.d(LOGTAG, "processStreamError");
458 }
459
460 private void sendStartStream() {
461 Tag stream = Tag.start("stream:stream");
462 stream.setAttribute("from", account.getJid());
463 stream.setAttribute("to", account.getServer());
464 stream.setAttribute("version", "1.0");
465 stream.setAttribute("xml:lang", "en");
466 stream.setAttribute("xmlns", "jabber:client");
467 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
468 tagWriter.writeTag(stream);
469 }
470
471 private String nextRandomId() {
472 return new BigInteger(50, random).toString(32);
473 }
474
475 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
476 String id = nextRandomId();
477 packet.setAttribute("id", id);
478 tagWriter.writeElement(packet);
479 if (callback != null) {
480 packetCallbacks.put(id, callback);
481 }
482 }
483
484 public void sendMessagePacket(MessagePacket packet) {
485 this.sendMessagePacket(packet, null);
486 }
487
488 public void sendMessagePacket(MessagePacket packet,
489 OnMessagePacketReceived 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 sendPresencePacket(PresencePacket packet) {
499 this.sendPresencePacket(packet, null);
500 }
501
502 public PresencePacket sendPresencePacket(PresencePacket packet,
503 OnPresencePacketReceived 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 return packet;
511 }
512
513 public void setOnMessagePacketReceivedListener(
514 OnMessagePacketReceived listener) {
515 this.messageListener = listener;
516 }
517
518 public void setOnUnregisteredIqPacketReceivedListener(
519 OnIqPacketReceived listener) {
520 this.unregisteredIqListener = listener;
521 }
522
523 public void setOnPresencePacketReceivedListener(
524 OnPresencePacketReceived listener) {
525 this.presenceListener = listener;
526 }
527
528 public void setOnStatusChangedListener(OnStatusChanged listener) {
529 this.statusListener = listener;
530 }
531
532 public void disconnect() {
533 shouldConnect = false;
534 tagWriter.writeTag(Tag.end("stream:stream"));
535 }
536}