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