1package de.gultsch.chat.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.Hashtable;
11
12import javax.net.ssl.SSLSocket;
13import javax.net.ssl.SSLSocketFactory;
14
15import org.xmlpull.v1.XmlPullParserException;
16
17import android.os.Bundle;
18import android.os.PowerManager;
19import android.util.Log;
20import de.gultsch.chat.entities.Account;
21import de.gultsch.chat.utils.DNSHelper;
22import de.gultsch.chat.utils.SASL;
23import de.gultsch.chat.xml.Element;
24import de.gultsch.chat.xml.Tag;
25import de.gultsch.chat.xml.XmlReader;
26import de.gultsch.chat.xml.TagWriter;
27
28public class XmppConnection implements Runnable {
29
30 protected Account account;
31 private static final String LOGTAG = "xmppService";
32
33 private PowerManager.WakeLock wakeLock;
34
35 private SecureRandom random = new SecureRandom();
36
37 private Socket socket;
38 private XmlReader tagReader;
39 private TagWriter tagWriter;
40
41 private boolean isTlsEncrypted = false;
42 private boolean isAuthenticated = false;
43 //private boolean shouldUseTLS = false;
44 private boolean shouldConnect = true;
45 private boolean shouldBind = true;
46 private boolean shouldAuthenticate = true;
47 private Element streamFeatures;
48
49 private static final int PACKET_IQ = 0;
50 private static final int PACKET_MESSAGE = 1;
51 private static final int PACKET_PRESENCE = 2;
52
53 private Hashtable<String, OnIqPacketReceived> iqPacketCallbacks = new Hashtable<String, OnIqPacketReceived>();
54 private OnPresencePacketReceived presenceListener = null;
55 private OnIqPacketReceived unregisteredIqListener = null;
56 private OnMessagePacketReceived messageListener = null;
57 private OnStatusChanged statusListener = null;
58
59 public XmppConnection(Account account, PowerManager pm) {
60 this.account = account;
61 wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
62 "XmppConnection");
63 tagReader = new XmlReader(wakeLock);
64 tagWriter = new TagWriter();
65 }
66
67 protected void connect() {
68 try {
69 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
70 String srvRecordServer = namePort.getString("name");
71 int srvRecordPort = namePort.getInt("port");
72 if (srvRecordServer!=null) {
73 Log.d(LOGTAG,account.getJid()+": using values from dns "+srvRecordServer+":"+srvRecordPort);
74 socket = new Socket(srvRecordServer,srvRecordPort);
75 } else {
76 socket = new Socket(account.getServer(), 5222);
77 }
78 OutputStream out = socket.getOutputStream();
79 tagWriter.setOutputStream(out);
80 InputStream in = socket.getInputStream();
81 tagReader.setInputStream(in);
82 tagWriter.beginDocument();
83 sendStartStream();
84 Tag nextTag;
85 while ((nextTag = tagReader.readTag()) != null) {
86 if (nextTag.isStart("stream")) {
87 processStream(nextTag);
88 break;
89 } else {
90 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
91 return;
92 }
93 }
94 if (socket.isConnected()) {
95 socket.close();
96 }
97 } catch (UnknownHostException e) {
98 account.setStatus(Account.STATUS_SERVER_NOT_FOUND);
99 if (statusListener!=null) {
100 statusListener.onStatusChanged(account);
101 }
102 return;
103 } catch (IOException e) {
104 Log.d(LOGTAG,"bla "+e.getMessage());
105 if (shouldConnect) {
106 Log.d(LOGTAG,account.getJid()+": connection lost");
107 account.setStatus(Account.STATUS_OFFLINE);
108 if (statusListener!=null) {
109 statusListener.onStatusChanged(account);
110 }
111 }
112 } catch (XmlPullParserException e) {
113 Log.d(LOGTAG,"xml exception "+e.getMessage());
114 return;
115 }
116
117 }
118
119 @Override
120 public void run() {
121 shouldConnect = true;
122 while(shouldConnect) {
123 connect();
124 try {
125 if (shouldConnect) {
126 Thread.sleep(30000);
127 }
128 } catch (InterruptedException e) {
129 // TODO Auto-generated catch block
130 e.printStackTrace();
131 }
132 }
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()+": read success tag in stream. reset again");
149 tagReader.readTag();
150 tagReader.reset();
151 sendStartStream();
152 processStream(tagReader.readTag());
153 break;
154 } else if(nextTag.isStart("failure")) {
155 Element failure = tagReader.readElement(nextTag);
156 Log.d(LOGTAG,"read failure element"+failure.toString());
157 account.setStatus(Account.STATUS_UNAUTHORIZED);
158 if (statusListener!=null) {
159 statusListener.onStatusChanged(account);
160 }
161 tagWriter.writeTag(Tag.end("stream"));
162 } else if (nextTag.isStart("iq")) {
163 processIq(nextTag);
164 } else if (nextTag.isStart("message")) {
165 processMessage(nextTag);
166 } else if (nextTag.isStart("presence")) {
167 processPresence(nextTag);
168 } else {
169 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
170 + " as child of " + currentTag.getName());
171 }
172 nextTag = tagReader.readTag();
173 }
174 if (account.getStatus() == Account.STATUS_ONLINE) {
175 account.setStatus(Account.STATUS_OFFLINE);
176 if (statusListener!=null) {
177 statusListener.onStatusChanged(account);
178 }
179 }
180 }
181
182 private Element processPacket(Tag currentTag, int packetType) throws XmlPullParserException, IOException {
183 Element element;
184 switch (packetType) {
185 case PACKET_IQ:
186 element = new IqPacket();
187 break;
188 case PACKET_MESSAGE:
189 element = new MessagePacket();
190 break;
191 case PACKET_PRESENCE:
192 element = new PresencePacket();
193 break;
194 default:
195 return null;
196 }
197 element.setAttributes(currentTag.getAttributes());
198 Tag nextTag = tagReader.readTag();
199 while(!nextTag.isEnd(element.getName())) {
200 if (!nextTag.isNo()) {
201 Element child = tagReader.readElement(nextTag);
202 element.addChild(child);
203 }
204 nextTag = tagReader.readTag();
205 }
206 return element;
207 }
208
209
210 private IqPacket processIq(Tag currentTag) throws XmlPullParserException, IOException {
211 IqPacket packet = (IqPacket) processPacket(currentTag,PACKET_IQ);
212 if (iqPacketCallbacks.containsKey(packet.getId())) {
213 iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account,packet);
214 iqPacketCallbacks.remove(packet.getId());
215 } else if (this.unregisteredIqListener != null) {
216 this.unregisteredIqListener.onIqPacketReceived(account,packet);
217 }
218 return packet;
219 }
220
221 private void processMessage(Tag currentTag) throws XmlPullParserException, IOException {
222 MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
223 if (this.messageListener != null) {
224 this.messageListener.onMessagePacketReceived(account,packet);
225 }
226 }
227
228 private void processPresence(Tag currentTag) throws XmlPullParserException, IOException {
229 PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
230 if (this.presenceListener != null) {
231 this.presenceListener.onPresencePacketReceived(account,packet);
232 }
233 }
234
235 private void sendStartTLS() {
236 Tag startTLS = Tag.empty("starttls");
237 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
238 Log.d(LOGTAG,account.getJid()+": sending starttls");
239 tagWriter.writeTag(startTLS);
240 }
241
242 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
243 IOException {
244 Tag nextTag = tagReader.readTag(); // should be proceed end tag
245 Log.d(LOGTAG,account.getJid()+": now switch to ssl");
246 SSLSocket sslSocket;
247 try {
248 sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
249 .getDefault()).createSocket(socket, socket.getInetAddress()
250 .getHostAddress(), socket.getPort(), true);
251 tagReader.setInputStream(sslSocket.getInputStream());
252 Log.d(LOGTAG, "reset inputstream");
253 tagWriter.setOutputStream(sslSocket.getOutputStream());
254 Log.d(LOGTAG, "switch over seemed to work");
255 isTlsEncrypted = true;
256 sendStartStream();
257 processStream(tagReader.readTag());
258 sslSocket.close();
259 } catch (IOException e) {
260 Log.d(LOGTAG, account.getJid()+": error on ssl '" + e.getMessage()+"'");
261 }
262 }
263
264 private void sendSaslAuth() throws IOException, XmlPullParserException {
265 String saslString = SASL.plain(account.getUsername(),
266 account.getPassword());
267 Element auth = new Element("auth");
268 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
269 auth.setAttribute("mechanism", "PLAIN");
270 auth.setContent(saslString);
271 Log.d(LOGTAG,account.getJid()+": sending sasl "+auth.toString());
272 tagWriter.writeElement(auth);
273 }
274
275 private void processStreamFeatures(Tag currentTag)
276 throws XmlPullParserException, IOException {
277 this.streamFeatures = tagReader.readElement(currentTag);
278 Log.d(LOGTAG,account.getJid()+": process stream features "+streamFeatures);
279 if (this.streamFeatures.hasChild("starttls")&&account.isOptionSet(Account.OPTION_USETLS)) {
280 sendStartTLS();
281 } else if (this.streamFeatures.hasChild("mechanisms")&&shouldAuthenticate) {
282 sendSaslAuth();
283 }
284 if (this.streamFeatures.hasChild("bind")&&shouldBind) {
285 sendBindRequest();
286 if (this.streamFeatures.hasChild("session")) {
287 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
288 Element session = new Element("session");
289 session.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-session");
290 session.setContent("");
291 startSession.addChild(session);
292 sendIqPacket(startSession, null);
293 tagWriter.writeElement(startSession);
294 }
295 Element presence = new Element("presence");
296
297 tagWriter.writeElement(presence);
298 }
299 }
300
301 private void sendBindRequest() throws IOException {
302 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
303 Element bind = new Element("bind");
304 bind.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-bind");
305 iq.addChild(bind);
306 this.sendIqPacket(iq, new OnIqPacketReceived() {
307 @Override
308 public void onIqPacketReceived(Account account, IqPacket packet) {
309 String resource = packet.findChild("bind").findChild("jid").getContent().split("/")[1];
310 account.setResource(resource);
311 account.setStatus(Account.STATUS_ONLINE);
312 if (statusListener!=null) {
313 statusListener.onStatusChanged(account);
314 }
315 }
316 });
317 }
318
319 private void processStreamError(Tag currentTag) {
320 Log.d(LOGTAG, "processStreamError");
321 }
322
323 private void sendStartStream() {
324 Tag stream = Tag.start("stream");
325 stream.setAttribute("from", account.getJid());
326 stream.setAttribute("to", account.getServer());
327 stream.setAttribute("version", "1.0");
328 stream.setAttribute("xml:lang", "en");
329 stream.setAttribute("xmlns", "jabber:client");
330 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
331 tagWriter.writeTag(stream);
332 }
333
334 private String nextRandomId() {
335 return new BigInteger(50, random).toString(32);
336 }
337
338 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
339 String id = nextRandomId();
340 packet.setAttribute("id",id);
341 tagWriter.writeElement(packet);
342 if (callback != null) {
343 iqPacketCallbacks.put(id, callback);
344 }
345 Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString());
346 }
347
348 public void sendMessagePacket(MessagePacket packet){
349 tagWriter.writeElement(packet);
350 }
351
352 public void sendPresencePacket(PresencePacket packet) {
353 tagWriter.writeElement(packet);
354 Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString());
355 }
356
357 public void setOnMessagePacketReceivedListener(OnMessagePacketReceived listener) {
358 this.messageListener = listener;
359 }
360
361 public void setOnUnregisteredIqPacketReceivedListener(OnIqPacketReceived listener) {
362 this.unregisteredIqListener = listener;
363 }
364
365 public void setOnPresencePacketReceivedListener(OnPresencePacketReceived listener) {
366 this.presenceListener = listener;
367 }
368
369 public void setOnStatusChangedListener(OnStatusChanged listener) {
370 this.statusListener = listener;
371 }
372
373 public void disconnect() {
374 shouldConnect = false;
375 tagWriter.writeTag(Tag.end("stream"));
376 }
377}