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