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, OnIqPacketReceived> iqPacketCallbacks = new Hashtable<String, OnIqPacketReceived>();
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 IqPacket processIq(Tag currentTag) throws XmlPullParserException,
216 IOException {
217 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
218 if (iqPacketCallbacks.containsKey(packet.getId())) {
219 iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account,
220 packet);
221 iqPacketCallbacks.remove(packet.getId());
222 } else if (this.unregisteredIqListener != null) {
223 this.unregisteredIqListener.onIqPacketReceived(account, packet);
224 }
225 return packet;
226 }
227
228 private void processMessage(Tag currentTag) throws XmlPullParserException,
229 IOException {
230 MessagePacket packet = (MessagePacket) processPacket(currentTag,
231 PACKET_MESSAGE);
232 if (this.messageListener != null) {
233 this.messageListener.onMessagePacketReceived(account, packet);
234 }
235 }
236
237 private void processPresence(Tag currentTag) throws XmlPullParserException,
238 IOException {
239 PresencePacket packet = (PresencePacket) processPacket(currentTag,
240 PACKET_PRESENCE);
241 if (this.presenceListener != null) {
242 this.presenceListener.onPresencePacketReceived(account, packet);
243 }
244 }
245
246 private void sendStartTLS() {
247 Tag startTLS = Tag.empty("starttls");
248 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
249 Log.d(LOGTAG, account.getJid() + ": sending starttls");
250 tagWriter.writeTag(startTLS);
251 }
252
253 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
254 IOException {
255 Tag nextTag = tagReader.readTag(); // should be proceed end tag
256 Log.d(LOGTAG, account.getJid() + ": now switch to ssl");
257 SSLSocket sslSocket;
258 try {
259 sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
260 .getDefault()).createSocket(socket, socket.getInetAddress()
261 .getHostAddress(), socket.getPort(), true);
262 tagReader.setInputStream(sslSocket.getInputStream());
263 Log.d(LOGTAG, "reset inputstream");
264 tagWriter.setOutputStream(sslSocket.getOutputStream());
265 Log.d(LOGTAG, "switch over seemed to work");
266 isTlsEncrypted = true;
267 sendStartStream();
268 processStream(tagReader.readTag());
269 sslSocket.close();
270 } catch (IOException e) {
271 Log.d(LOGTAG,
272 account.getJid() + ": error on ssl '" + e.getMessage()
273 + "'");
274 }
275 }
276
277 private void sendSaslAuth() throws IOException, XmlPullParserException {
278 String saslString = SASL.plain(account.getUsername(),
279 account.getPassword());
280 Element auth = new Element("auth");
281 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
282 auth.setAttribute("mechanism", "PLAIN");
283 auth.setContent(saslString);
284 Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString());
285 tagWriter.writeElement(auth);
286 }
287
288 private void processStreamFeatures(Tag currentTag)
289 throws XmlPullParserException, IOException {
290 this.streamFeatures = tagReader.readElement(currentTag);
291 Log.d(LOGTAG, account.getJid() + ": process stream features "
292 + streamFeatures);
293 if (this.streamFeatures.hasChild("starttls")
294 && account.isOptionSet(Account.OPTION_USETLS)) {
295 sendStartTLS();
296 } else if (this.streamFeatures.hasChild("mechanisms")
297 && shouldAuthenticate) {
298 sendSaslAuth();
299 }
300 if (this.streamFeatures.hasChild("bind") && shouldBind) {
301 sendBindRequest();
302 if (this.streamFeatures.hasChild("session")) {
303 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
304 Element session = new Element("session");
305 session.setAttribute("xmlns",
306 "urn:ietf:params:xml:ns:xmpp-session");
307 session.setContent("");
308 startSession.addChild(session);
309 sendIqPacket(startSession, null);
310 tagWriter.writeElement(startSession);
311 }
312 Element presence = new Element("presence");
313
314 tagWriter.writeElement(presence);
315 }
316 }
317
318 private void sendBindRequest() throws IOException {
319 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
320 Element bind = new Element("bind");
321 bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
322 iq.addChild(bind);
323 this.sendIqPacket(iq, new OnIqPacketReceived() {
324 @Override
325 public void onIqPacketReceived(Account account, IqPacket packet) {
326 String resource = packet.findChild("bind").findChild("jid")
327 .getContent().split("/")[1];
328 account.setResource(resource);
329 account.setStatus(Account.STATUS_ONLINE);
330 if (statusListener != null) {
331 statusListener.onStatusChanged(account);
332 }
333 sendServiceDiscovery();
334 }
335 });
336 }
337
338 private void sendServiceDiscovery() {
339 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
340 iq.setAttribute("to", account.getServer());
341 Element query = new Element("query");
342 query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
343 iq.addChild(query);
344 this.sendIqPacket(iq, new OnIqPacketReceived() {
345
346 @Override
347 public void onIqPacketReceived(Account account, IqPacket packet) {
348 if (packet.hasChild("query")) {
349 List<Element> elements = packet.findChild("query")
350 .getChildren();
351 for (int i = 0; i < elements.size(); ++i) {
352 if (elements.get(i).getName().equals("feature")) {
353 discoFeatures.add(elements.get(i).getAttribute(
354 "var"));
355 }
356 }
357 }
358 if (discoFeatures.contains("urn:xmpp:carbons:2")) {
359 sendEnableCarbons();
360 }
361 }
362 });
363 }
364
365 private void sendEnableCarbons() {
366 Log.d(LOGTAG,account.getJid()+": enable carbons");
367 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
368 Element enable = new Element("enable");
369 enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
370 iq.addChild(enable);
371 this.sendIqPacket(iq, new OnIqPacketReceived() {
372
373 @Override
374 public void onIqPacketReceived(Account account, IqPacket packet) {
375 if (!packet.hasChild("error")) {
376 Log.d(LOGTAG,account.getJid()+": successfully enabled carbons");
377 } else {
378 Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString());
379 }
380 }
381 });
382 }
383
384 private void processStreamError(Tag currentTag) {
385 Log.d(LOGTAG, "processStreamError");
386 }
387
388 private void sendStartStream() {
389 Tag stream = Tag.start("stream:stream");
390 stream.setAttribute("from", account.getJid());
391 stream.setAttribute("to", account.getServer());
392 stream.setAttribute("version", "1.0");
393 stream.setAttribute("xml:lang", "en");
394 stream.setAttribute("xmlns", "jabber:client");
395 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
396 tagWriter.writeTag(stream);
397 }
398
399 private String nextRandomId() {
400 return new BigInteger(50, random).toString(32);
401 }
402
403 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
404 String id = nextRandomId();
405 packet.setAttribute("id", id);
406 tagWriter.writeElement(packet);
407 if (callback != null) {
408 iqPacketCallbacks.put(id, callback);
409 }
410 //Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString());
411 }
412
413 public void sendMessagePacket(MessagePacket packet) {
414 Log.d(LOGTAG,"sending message packet "+packet.toString());
415 tagWriter.writeElement(packet);
416 }
417
418 public void sendPresencePacket(PresencePacket packet) {
419 tagWriter.writeElement(packet);
420 Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString());
421 }
422
423 public void setOnMessagePacketReceivedListener(
424 OnMessagePacketReceived listener) {
425 this.messageListener = listener;
426 }
427
428 public void setOnUnregisteredIqPacketReceivedListener(
429 OnIqPacketReceived listener) {
430 this.unregisteredIqListener = listener;
431 }
432
433 public void setOnPresencePacketReceivedListener(
434 OnPresencePacketReceived listener) {
435 this.presenceListener = listener;
436 }
437
438 public void setOnStatusChangedListener(OnStatusChanged listener) {
439 this.statusListener = listener;
440 }
441
442 public void disconnect() {
443 shouldConnect = false;
444 tagWriter.writeTag(Tag.end("stream:stream"));
445 }
446}