blob: 05ffc67d1288085958b9ce0deb0e7fa0710af801 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import org.jivesoftware.smack.Connection.ListenerWrapper;
24import org.jivesoftware.smack.packet.*;
25import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
26import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
27import org.jivesoftware.smack.sasl.SASLMechanism.Success;
28import org.jivesoftware.smack.util.PacketParserUtils;
29
30import org.xmlpull.v1.XmlPullParserFactory;
31import org.xmlpull.v1.XmlPullParser;
32import org.xmlpull.v1.XmlPullParserException;
33
34import java.util.concurrent.*;
35
36/**
37 * Listens for XML traffic from the XMPP server and parses it into packet objects.
38 * The packet reader also invokes all packet listeners and collectors.<p>
39 *
40 * @see Connection#createPacketCollector
41 * @see Connection#addPacketListener
42 * @author Matt Tucker
43 */
44class PacketReader {
45
46 private Thread readerThread;
47 private ExecutorService listenerExecutor;
48
49 private XMPPConnection connection;
50 private XmlPullParser parser;
51 volatile boolean done;
52
53 private String connectionID = null;
54
55 protected PacketReader(final XMPPConnection connection) {
56 this.connection = connection;
57 this.init();
58 }
59
60 /**
61 * Initializes the reader in order to be used. The reader is initialized during the
62 * first connection and when reconnecting due to an abruptly disconnection.
63 */
64 protected void init() {
65 done = false;
66 connectionID = null;
67
68 readerThread = new Thread() {
69 public void run() {
70 parsePackets(this);
71 }
72 };
73 readerThread.setName("Smack Packet Reader (" + connection.connectionCounterValue + ")");
74 readerThread.setDaemon(true);
75
76 // Create an executor to deliver incoming packets to listeners. We'll use a single
77 // thread with an unbounded queue.
78 listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
79
80 public Thread newThread(Runnable runnable) {
81 Thread thread = new Thread(runnable,
82 "Smack Listener Processor (" + connection.connectionCounterValue + ")");
83 thread.setDaemon(true);
84 return thread;
85 }
86 });
87
88 resetParser();
89 }
90
91 /**
92 * Starts the packet reader thread and returns once a connection to the server
93 * has been established. A connection will be attempted for a maximum of five
94 * seconds. An XMPPException will be thrown if the connection fails.
95 *
96 * @throws XMPPException if the server fails to send an opening stream back
97 * for more than five seconds.
98 */
99 synchronized public void startup() throws XMPPException {
100 readerThread.start();
101 // Wait for stream tag before returning. We'll wait a couple of seconds before
102 // giving up and throwing an error.
103 try {
104 // A waiting thread may be woken up before the wait time or a notify
105 // (although this is a rare thing). Therefore, we continue waiting
106 // until either a connectionID has been set (and hence a notify was
107 // made) or the total wait time has elapsed.
108 int waitTime = SmackConfiguration.getPacketReplyTimeout();
109 wait(3 * waitTime);
110 }
111 catch (InterruptedException ie) {
112 // Ignore.
113 }
114 if (connectionID == null) {
115 throw new XMPPException("Connection failed. No response from server.");
116 }
117 else {
118 connection.connectionID = connectionID;
119 }
120 }
121
122 /**
123 * Shuts the packet reader down.
124 */
125 public void shutdown() {
126 // Notify connection listeners of the connection closing if done hasn't already been set.
127 if (!done) {
128 for (ConnectionListener listener : connection.getConnectionListeners()) {
129 try {
130 listener.connectionClosed();
131 }
132 catch (Exception e) {
133 // Catch and print any exception so we can recover
134 // from a faulty listener and finish the shutdown process
135 e.printStackTrace();
136 }
137 }
138 }
139 done = true;
140
141 // Shut down the listener executor.
142 listenerExecutor.shutdown();
143 }
144
145 /**
146 * Cleans up all resources used by the packet reader.
147 */
148 void cleanup() {
149 connection.recvListeners.clear();
150 connection.collectors.clear();
151 }
152
153 /**
154 * Resets the parser using the latest connection's reader. Reseting the parser is necessary
155 * when the plain connection has been secured or when a new opening stream element is going
156 * to be sent by the server.
157 */
158 private void resetParser() {
159 try {
160 parser = XmlPullParserFactory.newInstance().newPullParser();
161 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
162 parser.setInput(connection.reader);
163 }
164 catch (XmlPullParserException xppe) {
165 xppe.printStackTrace();
166 }
167 }
168
169 /**
170 * Parse top-level packets in order to process them further.
171 *
172 * @param thread the thread that is being used by the reader to parse incoming packets.
173 */
174 private void parsePackets(Thread thread) {
175 try {
176 int eventType = parser.getEventType();
177 do {
178 if (eventType == XmlPullParser.START_TAG) {
179 if (parser.getName().equals("message")) {
180 processPacket(PacketParserUtils.parseMessage(parser));
181 }
182 else if (parser.getName().equals("iq")) {
183 processPacket(PacketParserUtils.parseIQ(parser, connection));
184 }
185 else if (parser.getName().equals("presence")) {
186 processPacket(PacketParserUtils.parsePresence(parser));
187 }
188 // We found an opening stream. Record information about it, then notify
189 // the connectionID lock so that the packet reader startup can finish.
190 else if (parser.getName().equals("stream")) {
191 // Ensure the correct jabber:client namespace is being used.
192 if ("jabber:client".equals(parser.getNamespace(null))) {
193 // Get the connection id.
194 for (int i=0; i<parser.getAttributeCount(); i++) {
195 if (parser.getAttributeName(i).equals("id")) {
196 // Save the connectionID
197 connectionID = parser.getAttributeValue(i);
198 if (!"1.0".equals(parser.getAttributeValue("", "version"))) {
199 // Notify that a stream has been opened if the
200 // server is not XMPP 1.0 compliant otherwise make the
201 // notification after TLS has been negotiated or if TLS
202 // is not supported
203 releaseConnectionIDLock();
204 }
205 }
206 else if (parser.getAttributeName(i).equals("from")) {
207 // Use the server name that the server says that it is.
208 connection.config.setServiceName(parser.getAttributeValue(i));
209 }
210 }
211 }
212 }
213 else if (parser.getName().equals("error")) {
214 throw new XMPPException(PacketParserUtils.parseStreamError(parser));
215 }
216 else if (parser.getName().equals("features")) {
217 parseFeatures(parser);
218 }
219 else if (parser.getName().equals("proceed")) {
220 // Secure the connection by negotiating TLS
221 connection.proceedTLSReceived();
222 // Reset the state of the parser since a new stream element is going
223 // to be sent by the server
224 resetParser();
225 }
226 else if (parser.getName().equals("failure")) {
227 String namespace = parser.getNamespace(null);
228 if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) {
229 // TLS negotiation has failed. The server will close the connection
230 throw new Exception("TLS negotiation has failed");
231 }
232 else if ("http://jabber.org/protocol/compress".equals(namespace)) {
233 // Stream compression has been denied. This is a recoverable
234 // situation. It is still possible to authenticate and
235 // use the connection but using an uncompressed connection
236 connection.streamCompressionDenied();
237 }
238 else {
239 // SASL authentication has failed. The server may close the connection
240 // depending on the number of retries
241 final Failure failure = PacketParserUtils.parseSASLFailure(parser);
242 processPacket(failure);
243 connection.getSASLAuthentication().authenticationFailed();
244 }
245 }
246 else if (parser.getName().equals("challenge")) {
247 // The server is challenging the SASL authentication made by the client
248 String challengeData = parser.nextText();
249 processPacket(new Challenge(challengeData));
250 connection.getSASLAuthentication().challengeReceived(challengeData);
251 }
252 else if (parser.getName().equals("success")) {
253 processPacket(new Success(parser.nextText()));
254 // We now need to bind a resource for the connection
255 // Open a new stream and wait for the response
256 connection.packetWriter.openStream();
257 // Reset the state of the parser since a new stream element is going
258 // to be sent by the server
259 resetParser();
260 // The SASL authentication with the server was successful. The next step
261 // will be to bind the resource
262 connection.getSASLAuthentication().authenticated();
263 }
264 else if (parser.getName().equals("compressed")) {
265 // Server confirmed that it's possible to use stream compression. Start
266 // stream compression
267 connection.startStreamCompression();
268 // Reset the state of the parser since a new stream element is going
269 // to be sent by the server
270 resetParser();
271 }
272 }
273 else if (eventType == XmlPullParser.END_TAG) {
274 if (parser.getName().equals("stream")) {
275 // Disconnect the connection
276 connection.disconnect();
277 }
278 }
279 eventType = parser.next();
280 } while (!done && eventType != XmlPullParser.END_DOCUMENT && thread == readerThread);
281 }
282 catch (Exception e) {
283 // The exception can be ignored if the the connection is 'done'
284 // or if the it was caused because the socket got closed
285 if (!(done || connection.isSocketClosed())) {
286 // Close the connection and notify connection listeners of the
287 // error.
288 connection.notifyConnectionError(e);
289 }
290 }
291 }
292
293 /**
294 * Releases the connection ID lock so that the thread that was waiting can resume. The
295 * lock will be released when one of the following three conditions is met:<p>
296 *
297 * 1) An opening stream was sent from a non XMPP 1.0 compliant server
298 * 2) Stream features were received from an XMPP 1.0 compliant server that does not support TLS
299 * 3) TLS negotiation was successful
300 *
301 */
302 synchronized private void releaseConnectionIDLock() {
303 notify();
304 }
305
306 /**
307 * Processes a packet after it's been fully parsed by looping through the installed
308 * packet collectors and listeners and letting them examine the packet to see if
309 * they are a match with the filter.
310 *
311 * @param packet the packet to process.
312 */
313 private void processPacket(Packet packet) {
314 if (packet == null) {
315 return;
316 }
317
318 // Loop through all collectors and notify the appropriate ones.
319 for (PacketCollector collector: connection.getPacketCollectors()) {
320 collector.processPacket(packet);
321 }
322
323 // Deliver the incoming packet to listeners.
324 listenerExecutor.submit(new ListenerNotification(packet));
325 }
326
327 private void parseFeatures(XmlPullParser parser) throws Exception {
328 boolean startTLSReceived = false;
329 boolean startTLSRequired = false;
330 boolean done = false;
331 while (!done) {
332 int eventType = parser.next();
333
334 if (eventType == XmlPullParser.START_TAG) {
335 if (parser.getName().equals("starttls")) {
336 startTLSReceived = true;
337 }
338 else if (parser.getName().equals("mechanisms")) {
339 // The server is reporting available SASL mechanisms. Store this information
340 // which will be used later while logging (i.e. authenticating) into
341 // the server
342 connection.getSASLAuthentication()
343 .setAvailableSASLMethods(PacketParserUtils.parseMechanisms(parser));
344 }
345 else if (parser.getName().equals("bind")) {
346 // The server requires the client to bind a resource to the stream
347 connection.getSASLAuthentication().bindingRequired();
348 }
349 else if(parser.getName().equals("ver")){
350 connection.getConfiguration().setRosterVersioningAvailable(true);
351 }
352 // Set the entity caps node for the server if one is send
353 // See http://xmpp.org/extensions/xep-0115.html#stream
354 else if (parser.getName().equals("c")) {
355 String node = parser.getAttributeValue(null, "node");
356 String ver = parser.getAttributeValue(null, "ver");
357 if (ver != null && node != null) {
358 String capsNode = node + "#" + ver;
359 // In order to avoid a dependency from smack to smackx
360 // we have to set the services caps node in the connection
361 // and not directly in the EntityCapsManager
362 connection.setServiceCapsNode(capsNode);
363 }
364 }
365 else if (parser.getName().equals("session")) {
366 // The server supports sessions
367 connection.getSASLAuthentication().sessionsSupported();
368 }
369 else if (parser.getName().equals("compression")) {
370 // The server supports stream compression
371 connection.setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser));
372 }
373 else if (parser.getName().equals("register")) {
374 connection.getAccountManager().setSupportsAccountCreation(true);
375 }
376 }
377 else if (eventType == XmlPullParser.END_TAG) {
378 if (parser.getName().equals("starttls")) {
379 // Confirm the server that we want to use TLS
380 connection.startTLSReceived(startTLSRequired);
381 }
382 else if (parser.getName().equals("required") && startTLSReceived) {
383 startTLSRequired = true;
384 }
385 else if (parser.getName().equals("features")) {
386 done = true;
387 }
388 }
389 }
390
391 // If TLS is required but the server doesn't offer it, disconnect
392 // from the server and throw an error. First check if we've already negotiated TLS
393 // and are secure, however (features get parsed a second time after TLS is established).
394 if (!connection.isSecureConnection()) {
395 if (!startTLSReceived && connection.getConfiguration().getSecurityMode() ==
396 ConnectionConfiguration.SecurityMode.required)
397 {
398 throw new XMPPException("Server does not support security (TLS), " +
399 "but security required by connection configuration.",
400 new XMPPError(XMPPError.Condition.forbidden));
401 }
402 }
403
404 // Release the lock after TLS has been negotiated or we are not insterested in TLS
405 if (!startTLSReceived || connection.getConfiguration().getSecurityMode() ==
406 ConnectionConfiguration.SecurityMode.disabled)
407 {
408 releaseConnectionIDLock();
409 }
410 }
411
412 /**
413 * A runnable to notify all listeners of a packet.
414 */
415 private class ListenerNotification implements Runnable {
416
417 private Packet packet;
418
419 public ListenerNotification(Packet packet) {
420 this.packet = packet;
421 }
422
423 public void run() {
424 for (ListenerWrapper listenerWrapper : connection.recvListeners.values()) {
425 listenerWrapper.notifyListener(packet);
426 }
427 }
428 }
429}