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.util.HashSet;
11import java.util.Hashtable;
12import java.util.List;
13
14import javax.net.ssl.SSLSocket;
15import javax.net.ssl.SSLSocketFactory;
16
17import org.xmlpull.v1.XmlPullParserException;
18
19import android.os.Bundle;
20import android.os.PowerManager;
21import android.util.Log;
22import eu.siacs.conversations.entities.Account;
23import eu.siacs.conversations.utils.DNSHelper;
24import eu.siacs.conversations.utils.SASL;
25import eu.siacs.conversations.xml.Element;
26import eu.siacs.conversations.xml.Tag;
27import eu.siacs.conversations.xml.TagWriter;
28import eu.siacs.conversations.xml.XmlReader;
29
30public class XmppConnection implements Runnable {
31
32 protected Account account;
33 private static final String LOGTAG = "xmppService";
34
35 private PowerManager.WakeLock wakeLock;
36
37 private SecureRandom random = new SecureRandom();
38
39 private Socket socket;
40 private XmlReader tagReader;
41 private TagWriter tagWriter;
42
43 private boolean isTlsEncrypted = false;
44 private boolean isAuthenticated = false;
45 // private boolean shouldUseTLS = false;
46 private boolean shouldConnect = true;
47 private boolean shouldBind = true;
48 private boolean shouldAuthenticate = true;
49 private Element streamFeatures;
50 private HashSet<String> discoFeatures = new HashSet<String>();
51
52 private static final int PACKET_IQ = 0;
53 private static final int PACKET_MESSAGE = 1;
54 private static final int PACKET_PRESENCE = 2;
55
56 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
57 private OnPresencePacketReceived presenceListener = null;
58 private OnIqPacketReceived unregisteredIqListener = null;
59 private OnMessagePacketReceived messageListener = null;
60 private OnStatusChanged statusListener = null;
61
62 public XmppConnection(Account account, PowerManager pm) {
63 this.account = account;
64 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
65 "XmppConnection");
66 tagReader = new XmlReader(wakeLock);
67 tagWriter = new TagWriter();
68 }
69
70 protected void connect() {
71 try {
72 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
73 String srvRecordServer = namePort.getString("name");
74 int srvRecordPort = namePort.getInt("port");
75 if (srvRecordServer != null) {
76 Log.d(LOGTAG, account.getJid() + ": using values from dns "
77 + srvRecordServer + ":" + srvRecordPort);
78 socket = new Socket(srvRecordServer, srvRecordPort);
79 } else {
80 socket = new Socket(account.getServer(), 5222);
81 }
82 OutputStream out = socket.getOutputStream();
83 tagWriter.setOutputStream(out);
84 InputStream in = socket.getInputStream();
85 tagReader.setInputStream(in);
86 tagWriter.beginDocument();
87 sendStartStream();
88 Tag nextTag;
89 while ((nextTag = tagReader.readTag()) != null) {
90 if (nextTag.isStart("stream")) {
91 processStream(nextTag);
92 break;
93 } else {
94 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
95 return;
96 }
97 }
98 if (socket.isConnected()) {
99 socket.close();
100 }
101 } catch (UnknownHostException e) {
102 account.setStatus(Account.STATUS_SERVER_NOT_FOUND);
103 if (statusListener != null) {
104 statusListener.onStatusChanged(account);
105 }
106 return;
107 } catch (IOException e) {
108 Log.d(LOGTAG, "bla " + e.getMessage());
109 if (shouldConnect) {
110 Log.d(LOGTAG, account.getJid() + ": connection lost");
111 account.setStatus(Account.STATUS_OFFLINE);
112 if (statusListener != null) {
113 statusListener.onStatusChanged(account);
114 }
115 }
116 } catch (XmlPullParserException e) {
117 Log.d(LOGTAG, "xml exception " + e.getMessage());
118 return;
119 }
120
121 }
122
123 @Override
124 public void run() {
125 shouldConnect = true;
126 while (shouldConnect) {
127 connect();
128 try {
129 if (shouldConnect) {
130 Thread.sleep(30000);
131 }
132 } catch (InterruptedException e) {
133 // TODO Auto-generated catch block
134 e.printStackTrace();
135 }
136 }
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 } else if (nextTag.isStart("proceed")) {
149 switchOverToTls(nextTag);
150 } else if (nextTag.isStart("success")) {
151 isAuthenticated = true;
152 Log.d(LOGTAG, account.getJid()
153 + ": read success tag in stream. reset again");
154 tagReader.readTag();
155 tagReader.reset();
156 sendStartStream();
157 processStream(tagReader.readTag());
158 break;
159 } else if (nextTag.isStart("failure")) {
160 Element failure = tagReader.readElement(nextTag);
161 Log.d(LOGTAG, "read failure element" + failure.toString());
162 account.setStatus(Account.STATUS_UNAUTHORIZED);
163 if (statusListener != null) {
164 statusListener.onStatusChanged(account);
165 }
166 tagWriter.writeTag(Tag.end("stream"));
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())).onIqPacketReceived(account,
221 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)).onMessagePacketReceived(account,
238 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)).onPresencePacketReceived(account,
254 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()+": successfully enabled carbons");
393 } else {
394 Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString());
395 }
396 }
397 });
398 }
399
400 private void processStreamError(Tag currentTag) {
401 Log.d(LOGTAG, "processStreamError");
402 }
403
404 private void sendStartStream() {
405 Tag stream = Tag.start("stream:stream");
406 stream.setAttribute("from", account.getJid());
407 stream.setAttribute("to", account.getServer());
408 stream.setAttribute("version", "1.0");
409 stream.setAttribute("xml:lang", "en");
410 stream.setAttribute("xmlns", "jabber:client");
411 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
412 tagWriter.writeTag(stream);
413 }
414
415 private String nextRandomId() {
416 return new BigInteger(50, random).toString(32);
417 }
418
419 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
420 String id = nextRandomId();
421 packet.setAttribute("id", id);
422 tagWriter.writeElement(packet);
423 if (callback != null) {
424 packetCallbacks.put(id, callback);
425 }
426 }
427
428 public void sendMessagePacket(MessagePacket packet) {
429 this.sendMessagePacket(packet, null);
430 }
431
432 public void sendMessagePacket(MessagePacket packet, OnMessagePacketReceived callback) {
433 String id = nextRandomId();
434 packet.setAttribute("id", id);
435 tagWriter.writeElement(packet);
436 if (callback != null) {
437 packetCallbacks.put(id, callback);
438 }
439 }
440
441 public void sendPresencePacket(PresencePacket packet) {
442 this.sendPresencePacket(packet, null);
443 }
444
445 public PresencePacket sendPresencePacket(PresencePacket packet, OnPresencePacketReceived callback) {
446 String id = nextRandomId();
447 packet.setAttribute("id", id);
448 tagWriter.writeElement(packet);
449 if (callback != null) {
450 packetCallbacks.put(id, callback);
451 }
452 return packet;
453 }
454
455 public void setOnMessagePacketReceivedListener(
456 OnMessagePacketReceived listener) {
457 this.messageListener = listener;
458 }
459
460 public void setOnUnregisteredIqPacketReceivedListener(
461 OnIqPacketReceived listener) {
462 this.unregisteredIqListener = listener;
463 }
464
465 public void setOnPresencePacketReceivedListener(
466 OnPresencePacketReceived listener) {
467 this.presenceListener = listener;
468 }
469
470 public void setOnStatusChangedListener(OnStatusChanged listener) {
471 this.statusListener = listener;
472 }
473
474 public void disconnect() {
475 shouldConnect = false;
476 tagWriter.writeTag(Tag.end("stream:stream"));
477 }
478}