mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-06-13 07:04:49 +02:00
d17f64ed9a
Fix a race condition in useCompression where the compression request was send outside the synchronized block, this could cause notify() to be called without a previoius call to wait(). s/streamCompressionDenied/streamCompressionNegotiationDone/ and re-use the method once the server ack'd stream compression. Also don't call notifyConnectionError() when requestStreamCompression() encounters an exception, instead throw the exception.
386 lines
18 KiB
Java
386 lines
18 KiB
Java
/**
|
|
*
|
|
* Copyright 2003-2007 Jive Software.
|
|
*
|
|
* 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 org.jivesoftware.smack.packet.IQ;
|
|
import org.jivesoftware.smack.packet.Packet;
|
|
import org.jivesoftware.smack.packet.Presence;
|
|
import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
|
|
import org.jivesoftware.smack.parsing.UnparsablePacket;
|
|
import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
|
|
import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure;
|
|
import org.jivesoftware.smack.sasl.SASLMechanism.Success;
|
|
import org.jivesoftware.smack.util.PacketParserUtils;
|
|
|
|
import org.jivesoftware.smack.SmackException.NoResponseException;
|
|
import org.jivesoftware.smack.SmackException.SecurityRequiredException;
|
|
import org.jivesoftware.smack.XMPPException.StreamErrorException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
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 XMPPConnection#createPacketCollector
|
|
* @see XMPPConnection#addPacketListener
|
|
* @author Matt Tucker
|
|
*/
|
|
class PacketReader {
|
|
|
|
private Thread readerThread;
|
|
|
|
private XMPPTCPConnection connection;
|
|
private XmlPullParser parser;
|
|
|
|
/**
|
|
* Set to true if the last features stanza from the server has been parsed. A XMPP connection
|
|
* handshake can invoke multiple features stanzas, e.g. when TLS is activated a second feature
|
|
* stanza is send by the server. This is set to true once the last feature stanza has been
|
|
* parsed.
|
|
*/
|
|
private volatile boolean lastFeaturesParsed;
|
|
|
|
volatile boolean done;
|
|
|
|
protected PacketReader(final XMPPTCPConnection connection) throws XmlPullParserException {
|
|
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.
|
|
*
|
|
* @throws XmlPullParserException if the parser could not be reset.
|
|
*/
|
|
protected void init() throws XmlPullParserException {
|
|
done = false;
|
|
lastFeaturesParsed = false;
|
|
|
|
readerThread = new Thread() {
|
|
public void run() {
|
|
parsePackets(this);
|
|
}
|
|
};
|
|
readerThread.setName("Smack Packet Reader (" + connection.connectionCounterValue + ")");
|
|
readerThread.setDaemon(true);
|
|
|
|
resetParser();
|
|
}
|
|
|
|
/**
|
|
* Starts the packet reader thread and returns once a connection to the server
|
|
* has been established or if the server's features could not be parsed within
|
|
* the connection's PacketReplyTimeout.
|
|
*
|
|
* @throws NoResponseException if the server fails to send an opening stream back
|
|
* within packetReplyTimeout.
|
|
*/
|
|
synchronized public void startup() throws NoResponseException {
|
|
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 the server's features have been parsed (and hence a notify was
|
|
// made) or the total wait time has elapsed.
|
|
wait(connection.getPacketReplyTimeout());
|
|
}
|
|
catch (InterruptedException ie) {
|
|
// Ignore.
|
|
}
|
|
if (!lastFeaturesParsed) {
|
|
throw new NoResponseException();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shuts the packet reader down.
|
|
*/
|
|
public void shutdown() {
|
|
// Notify connection listeners of the connection closing if done hasn't already been set.
|
|
if (!done) {
|
|
connection.callConnectionClosedListener();
|
|
}
|
|
done = true;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @throws XmlPullParserException XmlPullParserException if the parser could not be reset.
|
|
*/
|
|
private void resetParser() throws XmlPullParserException {
|
|
parser = XmlPullParserFactory.newInstance().newPullParser();
|
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
|
parser.setInput(connection.reader);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
int parserDepth = parser.getDepth();
|
|
ParsingExceptionCallback callback = connection.getParsingExceptionCallback();
|
|
if (parser.getName().equals("message")) {
|
|
Packet packet;
|
|
try {
|
|
packet = PacketParserUtils.parseMessage(parser);
|
|
} catch (Exception e) {
|
|
String content = PacketParserUtils.parseContentDepth(parser, parserDepth);
|
|
UnparsablePacket message = new UnparsablePacket(content, e);
|
|
if (callback != null) {
|
|
callback.handleUnparsablePacket(message);
|
|
}
|
|
continue;
|
|
}
|
|
connection.processPacket(packet);
|
|
}
|
|
else if (parser.getName().equals("iq")) {
|
|
IQ iq;
|
|
try {
|
|
iq = PacketParserUtils.parseIQ(parser, connection);
|
|
} catch (Exception e) {
|
|
String content = PacketParserUtils.parseContentDepth(parser, parserDepth);
|
|
UnparsablePacket message = new UnparsablePacket(content, e);
|
|
if (callback != null) {
|
|
callback.handleUnparsablePacket(message);
|
|
}
|
|
continue;
|
|
}
|
|
connection.processPacket(iq);
|
|
}
|
|
else if (parser.getName().equals("presence")) {
|
|
Presence presence;
|
|
try {
|
|
presence = PacketParserUtils.parsePresence(parser);
|
|
} catch (Exception e) {
|
|
String content = PacketParserUtils.parseContentDepth(parser, parserDepth);
|
|
UnparsablePacket message = new UnparsablePacket(content, e);
|
|
if (callback != null) {
|
|
callback.handleUnparsablePacket(message);
|
|
}
|
|
continue;
|
|
}
|
|
connection.processPacket(presence);
|
|
}
|
|
// 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
|
|
connection.connectionID = parser.getAttributeValue(i);
|
|
}
|
|
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 StreamErrorException(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")) {
|
|
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.streamCompressionNegotiationDone();
|
|
}
|
|
else {
|
|
// SASL authentication has failed. The server may close the connection
|
|
// depending on the number of retries
|
|
final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
|
|
connection.processPacket(failure);
|
|
connection.getSASLAuthentication().authenticationFailed(failure);
|
|
}
|
|
}
|
|
else if (parser.getName().equals("challenge")) {
|
|
// The server is challenging the SASL authentication made by the client
|
|
String challengeData = parser.nextText();
|
|
connection.processPacket(new Challenge(challengeData));
|
|
connection.getSASLAuthentication().challengeReceived(challengeData);
|
|
}
|
|
else if (parser.getName().equals("success")) {
|
|
connection.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 (Exception e) {
|
|
// The exception can be ignored if the the connection is 'done'
|
|
// or if the it was caused because the socket got closed
|
|
if (!(done || connection.isSocketClosed())) {
|
|
// Close the connection and notify connection listeners of the
|
|
// error.
|
|
connection.notifyConnectionError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void parseFeatures(XmlPullParser parser) throws Exception {
|
|
boolean startTLSReceived = false;
|
|
boolean startTLSRequired = false;
|
|
boolean done = false;
|
|
while (!done) {
|
|
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();
|
|
}
|
|
// Set the entity caps node for the server if one is send
|
|
// See http://xmpp.org/extensions/xep-0115.html#stream
|
|
else if (parser.getName().equals("c")) {
|
|
String node = parser.getAttributeValue(null, "node");
|
|
String ver = parser.getAttributeValue(null, "ver");
|
|
if (ver != null && node != null) {
|
|
String capsNode = node + "#" + ver;
|
|
// In order to avoid a dependency from smack to smackx
|
|
// we have to set the services caps node in the connection
|
|
// and not directly in the EntityCapsManager
|
|
connection.setServiceCapsNode(capsNode);
|
|
}
|
|
}
|
|
else if (parser.getName().equals("session")) {
|
|
// The server supports sessions
|
|
connection.getSASLAuthentication().sessionsSupported();
|
|
}
|
|
else if (parser.getName().equals("ver")) {
|
|
if (parser.getNamespace().equals("urn:xmpp:features:rosterver")) {
|
|
connection.setRosterVersioningSupported();
|
|
}
|
|
}
|
|
else if (parser.getName().equals("compression")) {
|
|
// The server supports stream compression
|
|
connection.setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser));
|
|
}
|
|
else if (parser.getName().equals("register")) {
|
|
AccountManager.getInstance(connection).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 SecurityRequiredException();
|
|
}
|
|
}
|
|
|
|
// Release the lock after TLS has been negotiated or we are not interested in TLS. If the
|
|
// server announced TLS and we choose to use it, by sending 'starttls', which the server
|
|
// replied with 'proceed', the server is required to send a new stream features element that
|
|
// "MUST NOT include the STARTTLS feature" (RFC6120 5.4.3.3. 5.). We are therefore save to
|
|
// release the connection lock once either TLS is disabled or we received a features stanza
|
|
// without starttls.
|
|
if (!startTLSReceived || connection.getConfiguration().getSecurityMode() ==
|
|
ConnectionConfiguration.SecurityMode.disabled)
|
|
{
|
|
lastFeaturesParsed = true;
|
|
// This synchronized block prevents this thread from calling notify() before the other
|
|
// thread had called wait() (it would cause an Exception if wait() hadn't been called)
|
|
synchronized (this) {
|
|
notify();
|
|
}
|
|
}
|
|
}
|
|
}
|