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