/** * $RCSfile$ * $Revision: 11616 $ * $Date: 2010-02-09 06:40:11 -0600 (Tue, 09 Feb 2010) $ * * Copyright 2003-2007 Jive Software. * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smack; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.jivesoftware.smack.Connection.ListenerWrapper; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.sasl.SASLMechanism.Challenge; import org.jivesoftware.smack.sasl.SASLMechanism.Failure; import org.jivesoftware.smack.sasl.SASLMechanism.Success; import org.jivesoftware.smack.util.PacketParserUtils; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; /** * Listens for XML traffic from the XMPP server and parses it into packet * objects. The packet reader also invokes all packet listeners and collectors. * <p> * * @see Connection#createPacketCollector * @see Connection#addPacketListener * @author Matt Tucker */ class PacketReader { /** * A runnable to notify all listeners of a packet. */ private class ListenerNotification implements Runnable { private final Packet packet; public ListenerNotification(Packet packet) { this.packet = packet; } @Override public void run() { for (final ListenerWrapper listenerWrapper : connection.recvListeners .values()) { listenerWrapper.notifyListener(packet); } } } private Thread readerThread; private ExecutorService listenerExecutor; private final XMPPConnection connection; private XmlPullParser parser; private boolean done; private String connectionID = null; private Semaphore connectionSemaphore; protected PacketReader(final XMPPConnection connection) { this.connection = connection; init(); } /** * Cleans up all resources used by the packet reader. */ void cleanup() { connection.recvListeners.clear(); connection.collectors.clear(); } /** * Initializes the reader in order to be used. The reader is initialized * during the first connection and when reconnecting due to an abruptly * disconnection. */ protected void init() { done = false; connectionID = null; readerThread = new Thread() { @Override public void run() { parsePackets(this); } }; readerThread.setName("Smack Packet Reader (" + connection.connectionCounterValue + ")"); readerThread.setDaemon(true); // Create an executor to deliver incoming packets to listeners. We'll // use a single // thread with an unbounded queue. listenerExecutor = Executors .newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { final Thread thread = new Thread(runnable, "Smack Listener Processor (" + connection.connectionCounterValue + ")"); thread.setDaemon(true); return thread; } }); resetParser(); } /** * Sends out a notification that there was an error with the connection and * closes the connection. * * @param e * the exception that causes the connection close event. */ void notifyConnectionError(Exception e) { done = true; // Closes the connection temporary. A reconnection is possible connection.shutdown(new Presence(Presence.Type.unavailable)); // Print the stack trace to help catch the problem e.printStackTrace(); // Notify connection listeners of the error. for (final ConnectionListener listener : connection .getConnectionListeners()) { try { listener.connectionClosedOnError(e); } catch (final Exception e2) { // Catch and print any exception so we can recover // from a faulty listener e2.printStackTrace(); } } } /** * Sends a notification indicating that the connection was reconnected * successfully. */ protected void notifyReconnection() { // Notify connection listeners of the reconnection. for (final ConnectionListener listener : connection .getConnectionListeners()) { try { listener.reconnectionSuccessful(); } catch (final Exception e) { // Catch and print any exception so we can recover // from a faulty listener e.printStackTrace(); } } } private void parseFeatures(XmlPullParser parser) throws Exception { boolean startTLSReceived = false; boolean startTLSRequired = false; boolean done = false; while (!done) { final int eventType = parser.next(); if (eventType == XmlPullParser.START_TAG) { if (parser.getName().equals("starttls")) { startTLSReceived = true; } else if (parser.getName().equals("mechanisms")) { // The server is reporting available SASL mechanisms. Store // this information // which will be used later while logging (i.e. // authenticating) into // the server connection.getSASLAuthentication().setAvailableSASLMethods( PacketParserUtils.parseMechanisms(parser)); } else if (parser.getName().equals("bind")) { // The server requires the client to bind a resource to the // stream connection.getSASLAuthentication().bindingRequired(); } else if (parser.getName().equals("session")) { // The server supports sessions connection.getSASLAuthentication().sessionsSupported(); } else if (parser.getName().equals("compression")) { // The server supports stream compression connection.setAvailableCompressionMethods(PacketParserUtils .parseCompressionMethods(parser)); } else if (parser.getName().equals("register")) { connection.getAccountManager().setSupportsAccountCreation( true); } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("starttls")) { // Confirm the server that we want to use TLS connection.startTLSReceived(startTLSRequired); } else if (parser.getName().equals("required") && startTLSReceived) { startTLSRequired = true; } else if (parser.getName().equals("features")) { done = true; } } } // If TLS is required but the server doesn't offer it, disconnect // from the server and throw an error. First check if we've already // negotiated TLS // and are secure, however (features get parsed a second time after TLS // is established). if (!connection.isSecureConnection()) { if (!startTLSReceived && connection.getConfiguration().getSecurityMode() == ConnectionConfiguration.SecurityMode.required) { throw new XMPPException( "Server does not support security (TLS), " + "but security required by connection configuration.", new XMPPError(XMPPError.Condition.forbidden)); } } // Release the lock after TLS has been negotiated or we are not // insterested in TLS if (!startTLSReceived || connection.getConfiguration().getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) { releaseConnectionIDLock(); } } /** * Parse top-level packets in order to process them further. * * @param thread * the thread that is being used by the reader to parse incoming * packets. */ private void parsePackets(Thread thread) { try { int eventType = parser.getEventType(); do { if (eventType == XmlPullParser.START_TAG) { if (parser.getName().equals("message")) { processPacket(PacketParserUtils.parseMessage(parser)); } else if (parser.getName().equals("iq")) { processPacket(PacketParserUtils.parseIQ(parser, connection)); } else if (parser.getName().equals("presence")) { processPacket(PacketParserUtils.parsePresence(parser)); } // We found an opening stream. Record information about it, // then notify // the connectionID lock so that the packet reader startup // can finish. else if (parser.getName().equals("stream")) { // Ensure the correct jabber:client namespace is being // used. if ("jabber:client".equals(parser.getNamespace(null))) { // Get the connection id. for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeName(i).equals("id")) { // Save the connectionID connectionID = parser.getAttributeValue(i); if (!"1.0".equals(parser.getAttributeValue( "", "version"))) { // Notify that a stream has been opened // if the // server is not XMPP 1.0 compliant // otherwise make the // notification after TLS has been // negotiated or if TLS // is not supported releaseConnectionIDLock(); } } else if (parser.getAttributeName(i).equals( "from")) { // Use the server name that the server says // that it is. connection.config.setServiceName(parser .getAttributeValue(i)); } } } } else if (parser.getName().equals("error")) { throw new XMPPException( PacketParserUtils.parseStreamError(parser)); } else if (parser.getName().equals("features")) { parseFeatures(parser); } else if (parser.getName().equals("proceed")) { // Secure the connection by negotiating TLS connection.proceedTLSReceived(); // Reset the state of the parser since a new stream // element is going // to be sent by the server resetParser(); } else if (parser.getName().equals("failure")) { final String namespace = parser.getNamespace(null); if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) { // TLS negotiation has failed. The server will close // the connection throw new Exception("TLS negotiation has failed"); } else if ("http://jabber.org/protocol/compress" .equals(namespace)) { // Stream compression has been denied. This is a // recoverable // situation. It is still possible to authenticate // and // use the connection but using an uncompressed // connection connection.streamCompressionDenied(); } else { // SASL authentication has failed. The server may // close the connection // depending on the number of retries final Failure failure = PacketParserUtils .parseSASLFailure(parser); processPacket(failure); connection.getSASLAuthentication() .authenticationFailed( failure.getCondition()); } } else if (parser.getName().equals("challenge")) { // The server is challenging the SASL authentication // made by the client final String challengeData = parser.nextText(); processPacket(new Challenge(challengeData)); connection.getSASLAuthentication().challengeReceived( challengeData); } else if (parser.getName().equals("success")) { processPacket(new Success(parser.nextText())); // We now need to bind a resource for the connection // Open a new stream and wait for the response connection.packetWriter.openStream(); // Reset the state of the parser since a new stream // element is going // to be sent by the server resetParser(); // The SASL authentication with the server was // successful. The next step // will be to bind the resource connection.getSASLAuthentication().authenticated(); } else if (parser.getName().equals("compressed")) { // Server confirmed that it's possible to use stream // compression. Start // stream compression connection.startStreamCompression(); // Reset the state of the parser since a new stream // element is going // to be sent by the server resetParser(); } } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("stream")) { // Disconnect the connection connection.disconnect(); } } eventType = parser.next(); } while (!done && eventType != XmlPullParser.END_DOCUMENT && thread == readerThread); } catch (final Exception e) { if (!done) { // Close the connection and notify connection listeners of the // error. notifyConnectionError(e); } } } /** * Processes a packet after it's been fully parsed by looping through the * installed packet collectors and listeners and letting them examine the * packet to see if they are a match with the filter. * * @param packet * the packet to process. */ private void processPacket(Packet packet) { if (packet == null) { return; } // Loop through all collectors and notify the appropriate ones. for (final PacketCollector collector : connection.getPacketCollectors()) { collector.processPacket(packet); } // Deliver the incoming packet to listeners. listenerExecutor.submit(new ListenerNotification(packet)); } /** * Releases the connection ID lock so that the thread that was waiting can * resume. The lock will be released when one of the following three * conditions is met: * <p> * * 1) An opening stream was sent from a non XMPP 1.0 compliant server 2) * Stream features were received from an XMPP 1.0 compliant server that does * not support TLS 3) TLS negotiation was successful * */ private void releaseConnectionIDLock() { connectionSemaphore.release(); } /** * Resets the parser using the latest connection's reader. Reseting the * parser is necessary when the plain connection has been secured or when a * new opening stream element is going to be sent by the server. */ private void resetParser() { try { parser = new MXParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(connection.reader); } catch (final XmlPullParserException xppe) { xppe.printStackTrace(); } } /** * Shuts the packet reader down. */ public void shutdown() { // Notify connection listeners of the connection closing if done hasn't // already been set. if (!done) { for (final ConnectionListener listener : connection .getConnectionListeners()) { try { listener.connectionClosed(); } catch (final Exception e) { // Cath and print any exception so we can recover // from a faulty listener and finish the shutdown process e.printStackTrace(); } } } done = true; // Shut down the listener executor. listenerExecutor.shutdown(); } /** * Starts the packet reader thread and returns once a connection to the * server has been established. A connection will be attempted for a maximum * of five seconds. An XMPPException will be thrown if the connection fails. * * @throws XMPPException * if the server fails to send an opening stream back for more * than five seconds. */ public void startup() throws XMPPException { connectionSemaphore = new Semaphore(1); readerThread.start(); // Wait for stream tag before returing. We'll wait a couple of seconds // before // giving up and throwing an error. try { connectionSemaphore.acquire(); // A waiting thread may be woken up before the wait time or a notify // (although this is a rare thing). Therefore, we continue waiting // until either a connectionID has been set (and hence a notify was // made) or the total wait time has elapsed. final int waitTime = SmackConfiguration.getPacketReplyTimeout(); connectionSemaphore.tryAcquire(3 * waitTime, TimeUnit.MILLISECONDS); } catch (final InterruptedException ie) { // Ignore. } if (connectionID == null) { throw new XMPPException( "Connection failed. No response from server."); } else { connection.connectionID = connectionID; } } }