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