org.jivesoftware.smack.PacketReader Maven / Gradle / Ivy
Show all versions of jamppa Show documentation
/**
*
* 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.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.dom4j.Element;
import org.dom4j.io.XPPPacketReader;
import org.jamppa.client.plugin.Plugin;
import org.jivesoftware.smack.Connection.ListenerWrapper;
import org.jivesoftware.smack.packet.Authentication;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.ping.packet.Ping;
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.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.PacketError.Condition;
import org.xmpp.packet.Presence;
import org.xmpp.packet.Roster;
/**
* Listens for XML traffic from the XMPP server and parses it into packet
* objects. The packet reader also invokes all packet listeners and collectors.
*
*
* @see Connection#createPacketCollector
* @see Connection#addPacketListener
* @author Matt Tucker
*/
class PacketReader {
private static Logger LOGGER = Logger.getLogger(PacketReader.class
.getName());
private Thread readerThread;
private ExecutorService listenerExecutor;
private XMPPConnection connection;
private XPPPacketReader innerReader;
private boolean reset = false;
volatile boolean done;
private String connectionID = null;
protected PacketReader(final XMPPConnection connection) {
this.connection = connection;
this.init();
}
/**
* 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() {
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() {
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable,
"Smack Listener Processor ("
+ connection.connectionCounterValue
+ ")");
thread.setDaemon(true);
return thread;
}
});
resetParser();
}
/**
* 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.
*/
synchronized public void startup() throws XMPPException {
final List errors = new LinkedList();
AbstractConnectionListener connectionErrorListener = new AbstractConnectionListener() {
@Override
public void connectionClosedOnError(Exception e) {
errors.add(e);
}
};
connection.addConnectionListener(connectionErrorListener);
readerThread.start();
// Wait for stream tag before returning. We'll wait a couple of seconds
// before
// giving up and throwing an error.
try {
// 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.
int waitTime = SmackConfiguration.getPacketReplyTimeout();
wait(3 * waitTime);
} catch (InterruptedException ie) {
// Ignore.
}
connection.removeConnectionListener(connectionErrorListener);
if (connectionID == null) {
throw new XMPPException(
"Connection failed. No response from server.");
} else if (!errors.isEmpty()) {
throw new XMPPException(errors.iterator().next());
} else {
connection.connectionID = connectionID;
}
}
/**
* 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 (ConnectionListener listener : connection
.getConnectionListeners()) {
try {
listener.connectionClosed();
} catch (Exception e) {
// Catch and print any exception so we can recover
// from a faulty listener and finish the shutdown process
LOGGER.log(Level.ERROR,
"Error in listener while closing connection", e);
}
}
}
done = true;
// Shut down the listener executor.
listenerExecutor.shutdown();
}
/**
* 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 {
innerReader = new XPPPacketReader();
innerReader.setXPPFactory(XmlPullParserFactory.newInstance());
innerReader.getXPPParser().setInput(connection.reader);
reset = true;
} catch (Exception xppe) {
LOGGER.log(Level.WARN, "Error while resetting parser", xppe);
}
}
private void startStream() throws XmlPullParserException, IOException {
XmlPullParser xpp = innerReader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
parseStreamStart(xpp);
}
private void parseStreamStart(XmlPullParser parser) {
if ("jabber:client".equals(parser.getNamespace(null))) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals("id")) {
connectionID = parser.getAttributeValue(i);
if (!"1.0".equals(parser.getAttributeValue("", "version"))) {
releaseConnectionIDLock();
}
} else if (parser.getAttributeName(i).equals("from")) {
connection.config.setServiceName(parser
.getAttributeValue(i));
}
}
}
}
/**
* 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 {
while (!done) {
if (reset) {
startStream();
LOGGER.debug("Started xmlstream...");
reset = false;
continue;
}
Element doc = innerReader.parseDocument().getRootElement();
if (doc == null) {
connection.disconnect();
LOGGER.debug("End of xmlstream.");
continue;
}
Packet packet = null;
LOGGER.debug("Processing packet " + doc.asXML());
packet = parseFromPlugins(doc, packet);
if (packet == null) {
packet = parseFromCore(doc);
}
if (packet != null) {
processPacket(packet);
}
}
} catch (Exception e) {
if (!done && !connection.isSocketClosed()) {
connection.notifyConnectionError(e);
if (!connection.isConnected()) {
releaseConnectionIDLock();
}
}
}
}
private Packet parseFromCore(Element doc) throws XMPPException,
IOException, XmlPullParserException, Exception {
String tag = doc.getName();
Packet packet = null;
if ("message".equals(tag)) {
packet = new Message(doc);
} else if ("presence".equals(tag)) {
packet = new Presence(doc);
} else if ("iq".equals(tag)) {
packet = parseIQ(doc);
} else if ("error".equals(tag)) {
throw new XMPPException(PacketParserUtils.parseStreamError(doc));
} else if ("features".equals(tag)) {
parseFeatures(doc);
} else if ("proceed".equals(tag)) {
connection.proceedTLSReceived();
resetParser();
} else if ("failure".equals(tag)) {
parseFailure(doc);
} else if ("challenge".equals(tag)) {
parseChallenge(doc);
} else if ("success".equals(tag)) {
parseSuccess(doc);
} else if ("compressed".equals(tag)) {
connection.startStreamCompression();
resetParser();
} else {
throw new XmlPullParserException("Unknown packet type was read: "
+ tag);
}
return packet;
}
private Packet parseFromPlugins(Element doc, Packet packet) {
for (Plugin plugin : connection.getPlugins()) {
packet = plugin.parse(doc);
if (packet != null) {
break;
}
}
return packet;
}
private void parseChallenge(Element doc) throws IOException {
String challengeData = doc.getText();
processPacket(new Challenge(doc));
connection.getSASLAuthentication().challengeReceived(challengeData);
}
private void parseSuccess(Element doc) throws IOException {
processPacket(new Success(doc));
connection.packetWriter.openStream();
resetParser();
connection.getSASLAuthentication().authenticated();
}
private void parseFailure(Element failureEl) throws Exception {
if ("urn:ietf:params:xml:ns:xmpp-tls".equals(failureEl.getNamespace()
.getURI())) {
throw new Exception("TLS negotiation has failed");
} else if ("http://jabber.org/protocol/compress".equals(failureEl
.getNamespace().getURI())) {
connection.streamCompressionDenied();
} else {
final Failure failure = new Failure(failureEl);
processPacket(failure);
connection.getSASLAuthentication().authenticationFailed(
failure.getCondition());
}
}
private IQ parseIQ(Element doc) {
Element pingEl = doc.element(Ping.ELEMENT);
if (pingEl != null
&& pingEl.getNamespace().getURI().equals(Ping.NAMESPACE)) {
return new Ping(doc);
}
Element bindEl = doc.element(Bind.ELEMENT);
if (bindEl != null
&& bindEl.getNamespace().getURI().equals(Bind.NAMESPACE)) {
return new Bind(doc);
}
Element query = doc.element("query");
if (query != null) {
if ("jabber:iq:roster".equals(query.getNamespaceURI())) {
return new Roster(doc);
}
if ("jabber:iq:auth".equals(query.getNamespaceURI())) {
return new Authentication(doc);
}
}
return new IQ(doc);
}
/**
* 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:
*
*
* 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
*
*/
synchronized private void releaseConnectionIDLock() {
notify();
}
/**
* 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 (PacketCollector collector : connection.getPacketCollectors()) {
collector.processPacket(packet);
}
// Deliver the incoming packet to listeners.
listenerExecutor.submit(new ListenerNotification(packet));
}
private void parseFeatures(Element doc) throws Exception {
boolean startTLSReceived = false;
Element startTLSEl = doc.element("starttls");
if (startTLSEl != null
&& startTLSEl.getNamespace().getURI()
.equals("urn:ietf:params:xml:ns:xmpp-tls")) {
startTLSReceived = true;
connection.startTLSReceived(startTLSEl.element("required") != null);
}
Element mechanismsEl = doc.element("mechanisms");
if (mechanismsEl != null
&& mechanismsEl.getNamespace().getURI()
.equals("urn:ietf:params:xml:ns:xmpp-sasl")) {
connection.getSASLAuthentication().setAvailableSASLMethods(
PacketParserUtils.parseMechanisms(mechanismsEl));
}
Element bindEl = doc.element("bind");
if (bindEl != null
&& bindEl.getNamespace().getURI()
.equals("urn:ietf:params:xml:ns:xmpp-bind")) {
connection.getSASLAuthentication().bindingRequired();
}
Element cEl = doc.element("c");
if (cEl != null
&& cEl.getNamespace().getURI()
.equals("http://jabber.org/protocol/caps")) {
String node = doc.attributeValue("node");
String ver = doc.attributeValue("ver");
if (ver != null && node != null) {
String capsNode = node + "#" + ver;
connection.setServiceCapsNode(capsNode);
}
}
Element sessionEl = doc.element("session");
if (sessionEl != null
&& sessionEl.getNamespace().getURI()
.equals("urn:ietf:params:xml:ns:xmpp-session")) {
connection.getSASLAuthentication().sessionsSupported();
}
Element verEl = doc.element("ver");
if (verEl != null
&& verEl.getNamespace().getURI()
.equals("urn:xmpp:features:rosterver")) {
connection.setRosterVersioningSupported();
}
Element compressionEl = doc.element("compression");
if (compressionEl != null
&& compressionEl.getNamespace().getURI()
.equals("http://jabber.org/features/compress")) {
connection.setAvailableCompressionMethods(PacketParserUtils
.parseCompressionMethods(compressionEl));
}
for (Plugin plugin : connection.getPlugins()) {
plugin.checkSupport(doc);
}
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 PacketError(Condition.forbidden));
}
}
if (!startTLSReceived
|| connection.getConfiguration().getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
releaseConnectionIDLock();
}
}
/**
* A runnable to notify all listeners of a packet.
*/
private class ListenerNotification implements Runnable {
private Packet packet;
public ListenerNotification(Packet packet) {
this.packet = packet;
}
public void run() {
for (ListenerWrapper listenerWrapper : connection.recvListeners
.values()) {
try {
listenerWrapper.notifyListener(packet);
} catch (Exception e) {
LOGGER.log(Level.ERROR, "Exception in packet listener", e);
}
}
}
}
}