mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-25 21:42:07 +01:00
Make PacketReader and PacketWriter nested classes
of XMPPTCPConnection.
This commit is contained in:
parent
6fd4bb850e
commit
ca4fbcf2b0
4 changed files with 546 additions and 683 deletions
|
@ -1,363 +0,0 @@
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 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.tcp;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.jivesoftware.smack.packet.Packet;
|
|
||||||
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.ConnectionConfiguration;
|
|
||||||
import org.jivesoftware.smack.SmackException;
|
|
||||||
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 SmackException {
|
|
||||||
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 SmackException if the parser could not be reset.
|
|
||||||
*/
|
|
||||||
protected void init() throws SmackException {
|
|
||||||
done = false;
|
|
||||||
lastFeaturesParsed = false;
|
|
||||||
|
|
||||||
readerThread = new Thread() {
|
|
||||||
public void run() {
|
|
||||||
parsePackets(this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
readerThread.setName("Smack Packet Reader (" + connection.getConnectionCounter() + ")");
|
|
||||||
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.
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
synchronized public void startup() throws NoResponseException, IOException {
|
|
||||||
readerThread.start();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait until either:
|
|
||||||
// - the servers last features stanza has been parsed
|
|
||||||
// - an exception is thrown while parsing
|
|
||||||
// - the timeout occurs
|
|
||||||
wait(connection.getPacketReplyTimeout());
|
|
||||||
}
|
|
||||||
catch (InterruptedException ie) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
if (!lastFeaturesParsed) {
|
|
||||||
connection.throwConnectionExceptionOrNoResponse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuts the packet reader down. This method simply sets the 'done' flag to true.
|
|
||||||
*/
|
|
||||||
public void shutdown() {
|
|
||||||
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 SmackException if the parser could not be reset.
|
|
||||||
*/
|
|
||||||
private void resetParser() throws SmackException {
|
|
||||||
try {
|
|
||||||
parser = XmlPullParserFactory.newInstance().newPullParser();
|
|
||||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
|
||||||
parser.setInput(connection.getReader());
|
|
||||||
}
|
|
||||||
catch (XmlPullParserException e) {
|
|
||||||
throw new SmackException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
String name = parser.getName();
|
|
||||||
ParsingExceptionCallback callback = connection.getParsingExceptionCallback();
|
|
||||||
Packet packet;
|
|
||||||
try {
|
|
||||||
packet = PacketParserUtils.parseStanza(parser, connection);
|
|
||||||
} catch (Exception e) {
|
|
||||||
String content = PacketParserUtils.parseContentDepth(parser, parserDepth);
|
|
||||||
UnparsablePacket message = new UnparsablePacket(content, e);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.handleUnparsablePacket(message);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (packet != null) {
|
|
||||||
connection.processPacket(packet);
|
|
||||||
}
|
|
||||||
// We found an opening stream. Record information about it, then notify
|
|
||||||
// the connectionID lock so that the packet reader startup can finish.
|
|
||||||
else if (name.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.setServiceName(parser.getAttributeValue(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (name.equals("error")) {
|
|
||||||
throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
|
|
||||||
}
|
|
||||||
else if (name.equals("features")) {
|
|
||||||
parseFeatures(parser);
|
|
||||||
}
|
|
||||||
else if (name.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 (name.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 (name.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 (name.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 (name.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())) {
|
|
||||||
synchronized(this) {
|
|
||||||
this.notify();
|
|
||||||
}
|
|
||||||
// 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.serverRequiresBinding();
|
|
||||||
}
|
|
||||||
// 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.serverSupportsSession();
|
|
||||||
}
|
|
||||||
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")) {
|
|
||||||
connection.serverSupportsAccountCreation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,238 +0,0 @@
|
||||||
/**
|
|
||||||
*
|
|
||||||
* 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.tcp;
|
|
||||||
|
|
||||||
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
|
||||||
import org.jivesoftware.smack.packet.Packet;
|
|
||||||
import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes packets to a XMPP server. Packets are sent using a dedicated thread. Packet
|
|
||||||
* interceptors can be registered to dynamically modify packets before they're actually
|
|
||||||
* sent. Packet listeners can be registered to listen for all outgoing packets.
|
|
||||||
*
|
|
||||||
* @see XMPPConnection#addPacketInterceptor
|
|
||||||
* @see XMPPConnection#addPacketSendingListener
|
|
||||||
*
|
|
||||||
* @author Matt Tucker
|
|
||||||
*/
|
|
||||||
class PacketWriter {
|
|
||||||
public static final int QUEUE_SIZE = 500;
|
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(PacketWriter.class.getName());
|
|
||||||
|
|
||||||
private final XMPPTCPConnection connection;
|
|
||||||
private final ArrayBlockingQueueWithShutdown<Packet> queue = new ArrayBlockingQueueWithShutdown<Packet>(QUEUE_SIZE, true);
|
|
||||||
|
|
||||||
private Thread writerThread;
|
|
||||||
private Writer writer;
|
|
||||||
|
|
||||||
volatile boolean done;
|
|
||||||
|
|
||||||
AtomicBoolean shutdownDone = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new packet writer with the specified connection.
|
|
||||||
*
|
|
||||||
* @param connection the connection.
|
|
||||||
*/
|
|
||||||
protected PacketWriter(XMPPTCPConnection connection) {
|
|
||||||
this.connection = connection;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the writer in order to be used. It is called at the first connection and also
|
|
||||||
* is invoked if the connection is disconnected by an error.
|
|
||||||
*/
|
|
||||||
protected void init() {
|
|
||||||
writer = connection.getWriter();
|
|
||||||
done = false;
|
|
||||||
shutdownDone.set(false);
|
|
||||||
|
|
||||||
queue.start();
|
|
||||||
writerThread = new Thread() {
|
|
||||||
public void run() {
|
|
||||||
writePackets(this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
writerThread.setName("Smack Packet Writer (" + connection.getConnectionCounter() + ")");
|
|
||||||
writerThread.setDaemon(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the specified packet to the server.
|
|
||||||
*
|
|
||||||
* @param packet the packet to send.
|
|
||||||
* @throws NotConnectedException
|
|
||||||
*/
|
|
||||||
public void sendPacket(Packet packet) throws NotConnectedException {
|
|
||||||
if (done) {
|
|
||||||
throw new NotConnectedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
queue.put(packet);
|
|
||||||
}
|
|
||||||
catch (InterruptedException ie) {
|
|
||||||
throw new NotConnectedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the packet writer thread and opens a connection to the server. The
|
|
||||||
* packet writer will continue writing packets until {@link #shutdown} or an
|
|
||||||
* error occurs.
|
|
||||||
*/
|
|
||||||
public void startup() {
|
|
||||||
writerThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setWriter(Writer writer) {
|
|
||||||
this.writer = writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shuts down the packet writer. Once this method has been called, no further
|
|
||||||
* packets will be written to the server.
|
|
||||||
*/
|
|
||||||
public void shutdown() {
|
|
||||||
done = true;
|
|
||||||
queue.shutdown();
|
|
||||||
synchronized(shutdownDone) {
|
|
||||||
if (!shutdownDone.get()) {
|
|
||||||
try {
|
|
||||||
shutdownDone.wait(connection.getPacketReplyTimeout());
|
|
||||||
}
|
|
||||||
catch (InterruptedException e) {
|
|
||||||
LOGGER.log(Level.WARNING, "shutdown", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next available packet from the queue for writing.
|
|
||||||
*
|
|
||||||
* @return the next packet for writing.
|
|
||||||
*/
|
|
||||||
private Packet nextPacket() {
|
|
||||||
if (done) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Packet packet = null;
|
|
||||||
try {
|
|
||||||
packet = queue.take();
|
|
||||||
}
|
|
||||||
catch (InterruptedException e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
return packet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writePackets(Thread thisThread) {
|
|
||||||
try {
|
|
||||||
// Open the stream.
|
|
||||||
openStream();
|
|
||||||
// Write out packets from the queue.
|
|
||||||
while (!done && (writerThread == thisThread)) {
|
|
||||||
Packet packet = nextPacket();
|
|
||||||
if (packet != null) {
|
|
||||||
writer.write(packet.toXML().toString());
|
|
||||||
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush out the rest of the queue. If the queue is extremely large, it's possible
|
|
||||||
// we won't have time to entirely flush it before the socket is forced closed
|
|
||||||
// by the shutdown process.
|
|
||||||
try {
|
|
||||||
while (!queue.isEmpty()) {
|
|
||||||
Packet packet = queue.remove();
|
|
||||||
writer.write(packet.toXML().toString());
|
|
||||||
}
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
LOGGER.log(Level.WARNING, "Exception flushing queue during shutdown, ignore and continue", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the queue contents (hopefully nothing is left).
|
|
||||||
queue.clear();
|
|
||||||
|
|
||||||
// Close the stream.
|
|
||||||
try {
|
|
||||||
writer.write("</stream:stream>");
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
LOGGER.log(Level.WARNING, "Exception writing closing stream element", e);
|
|
||||||
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
try {
|
|
||||||
writer.close();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownDone.set(true);
|
|
||||||
synchronized(shutdownDone) {
|
|
||||||
shutdownDone.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
// 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())) {
|
|
||||||
shutdown();
|
|
||||||
connection.notifyConnectionError(ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends to the server a new stream element. This operation may be requested several times
|
|
||||||
* so we need to encapsulate the logic in one place. This message will be sent while doing
|
|
||||||
* TLS, SASL and resource binding.
|
|
||||||
*
|
|
||||||
* @throws IOException If an error occurs while sending the stanza to the server.
|
|
||||||
*/
|
|
||||||
void openStream() throws IOException {
|
|
||||||
StringBuilder stream = new StringBuilder();
|
|
||||||
stream.append("<stream:stream");
|
|
||||||
stream.append(" to=\"").append(connection.getServiceName()).append("\"");
|
|
||||||
stream.append(" xmlns=\"jabber:client\"");
|
|
||||||
stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
|
|
||||||
stream.append(" version=\"1.0\">");
|
|
||||||
writer.write(stream.toString());
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -20,21 +20,31 @@ import org.jivesoftware.smack.AbstractXMPPConnection;
|
||||||
import org.jivesoftware.smack.ConnectionConfiguration;
|
import org.jivesoftware.smack.ConnectionConfiguration;
|
||||||
import org.jivesoftware.smack.ConnectionCreationListener;
|
import org.jivesoftware.smack.ConnectionCreationListener;
|
||||||
import org.jivesoftware.smack.ConnectionListener;
|
import org.jivesoftware.smack.ConnectionListener;
|
||||||
import org.jivesoftware.smack.SASLAuthentication;
|
|
||||||
import org.jivesoftware.smack.SmackConfiguration;
|
import org.jivesoftware.smack.SmackConfiguration;
|
||||||
import org.jivesoftware.smack.SmackException;
|
import org.jivesoftware.smack.SmackException;
|
||||||
import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
|
import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
|
||||||
import org.jivesoftware.smack.SmackException.NoResponseException;
|
import org.jivesoftware.smack.SmackException.NoResponseException;
|
||||||
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
||||||
import org.jivesoftware.smack.SmackException.ConnectionException;
|
import org.jivesoftware.smack.SmackException.ConnectionException;
|
||||||
|
import org.jivesoftware.smack.SmackException.SecurityRequiredException;
|
||||||
|
import org.jivesoftware.smack.XMPPException.StreamErrorException;
|
||||||
import org.jivesoftware.smack.XMPPConnection;
|
import org.jivesoftware.smack.XMPPConnection;
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
|
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
|
||||||
import org.jivesoftware.smack.packet.Packet;
|
import org.jivesoftware.smack.packet.Packet;
|
||||||
import org.jivesoftware.smack.packet.Presence;
|
import org.jivesoftware.smack.packet.Presence;
|
||||||
import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
|
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.ArrayBlockingQueueWithShutdown;
|
||||||
|
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||||
import org.jivesoftware.smack.util.StringUtils;
|
import org.jivesoftware.smack.util.StringUtils;
|
||||||
import org.jivesoftware.smack.util.dns.HostAddress;
|
import org.jivesoftware.smack.util.dns.HostAddress;
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||||||
|
|
||||||
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.KeyManager;
|
||||||
import javax.net.ssl.KeyManagerFactory;
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
@ -54,7 +64,6 @@ import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Reader;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
|
@ -67,6 +76,7 @@ import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
@ -84,11 +94,12 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
/**
|
/**
|
||||||
* The socket which is used for this connection.
|
* The socket which is used for this connection.
|
||||||
*/
|
*/
|
||||||
Socket socket;
|
private Socket socket;
|
||||||
|
|
||||||
String connectionID = null;
|
private String connectionID = null;
|
||||||
private String user = null;
|
private String user = null;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
|
||||||
// socketClosed is used concurrent
|
// socketClosed is used concurrent
|
||||||
// by XMPPTCPConnection, PacketReader, PacketWriter
|
// by XMPPTCPConnection, PacketReader, PacketWriter
|
||||||
private volatile boolean socketClosed = false;
|
private volatile boolean socketClosed = false;
|
||||||
|
@ -98,8 +109,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
|
|
||||||
private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback();
|
private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback();
|
||||||
|
|
||||||
PacketWriter packetWriter;
|
private PacketWriter packetWriter;
|
||||||
PacketReader packetReader;
|
private PacketReader packetReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection of available stream compression methods offered by the server.
|
* Collection of available stream compression methods offered by the server.
|
||||||
|
@ -193,6 +204,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
config.setCallbackHandler(callbackHandler);
|
config.setCallbackHandler(callbackHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getConnectionID() {
|
public String getConnectionID() {
|
||||||
if (!isConnected()) {
|
if (!isConnected()) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -200,6 +212,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
return connectionID;
|
return connectionID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getUser() {
|
public String getUser() {
|
||||||
if (!isAuthenticated()) {
|
if (!isAuthenticated()) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -335,22 +348,26 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
callConnectionAuthenticatedListener();
|
callConnectionAuthenticatedListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
return connected;
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isSecureConnection() {
|
public boolean isSecureConnection() {
|
||||||
return isUsingTLS();
|
return usingTLS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSocketClosed() {
|
public boolean isSocketClosed() {
|
||||||
return socketClosed;
|
return socketClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isAuthenticated() {
|
public boolean isAuthenticated() {
|
||||||
return authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isAnonymous() {
|
public boolean isAnonymous() {
|
||||||
return anonymous;
|
return anonymous;
|
||||||
}
|
}
|
||||||
|
@ -454,8 +471,8 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isFirstInitialization) {
|
if (isFirstInitialization) {
|
||||||
packetWriter = new PacketWriter(this);
|
packetWriter = new PacketWriter();
|
||||||
packetReader = new PacketReader(this);
|
packetReader = new PacketReader();
|
||||||
|
|
||||||
// If debugging is enabled, we should start the thread that will listen for
|
// If debugging is enabled, we should start the thread that will listen for
|
||||||
// all packets and then log them.
|
// all packets and then log them.
|
||||||
|
@ -534,16 +551,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
* TLS code below
|
* TLS code below
|
||||||
**********************************************/
|
**********************************************/
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the connection to the server has successfully negotiated TLS. Once TLS
|
|
||||||
* has been negotiatied the connection has been secured.
|
|
||||||
*
|
|
||||||
* @return true if the connection to the server has successfully negotiated TLS.
|
|
||||||
*/
|
|
||||||
public boolean isUsingTLS() {
|
|
||||||
return usingTLS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification message saying that the server supports TLS so confirm the server that we
|
* Notification message saying that the server supports TLS so confirm the server that we
|
||||||
* want to secure the connection.
|
* want to secure the connection.
|
||||||
|
@ -551,7 +558,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
* @param required true when the server indicates that TLS is required.
|
* @param required true when the server indicates that TLS is required.
|
||||||
* @throws IOException if an exception occurs.
|
* @throws IOException if an exception occurs.
|
||||||
*/
|
*/
|
||||||
void startTLSReceived(boolean required) throws IOException {
|
private void startTLSReceived(boolean required) throws IOException {
|
||||||
if (required && config.getSecurityMode() ==
|
if (required && config.getSecurityMode() ==
|
||||||
ConnectionConfiguration.SecurityMode.disabled) {
|
ConnectionConfiguration.SecurityMode.disabled) {
|
||||||
notifyConnectionError(new IllegalStateException(
|
notifyConnectionError(new IllegalStateException(
|
||||||
|
@ -574,7 +581,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
*
|
*
|
||||||
* @throws Exception if an exception occurs.
|
* @throws Exception if an exception occurs.
|
||||||
*/
|
*/
|
||||||
void proceedTLSReceived() throws Exception {
|
private void proceedTLSReceived() throws Exception {
|
||||||
SSLContext context = this.config.getCustomSSLContext();
|
SSLContext context = this.config.getCustomSSLContext();
|
||||||
KeyStore ks = null;
|
KeyStore ks = null;
|
||||||
KeyManager[] kms = null;
|
KeyManager[] kms = null;
|
||||||
|
@ -679,7 +686,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
*
|
*
|
||||||
* @param methods compression methods offered by the server.
|
* @param methods compression methods offered by the server.
|
||||||
*/
|
*/
|
||||||
void setAvailableCompressionMethods(Collection<String> methods) {
|
private void setAvailableCompressionMethods(Collection<String> methods) {
|
||||||
compressionMethods = methods;
|
compressionMethods = methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,6 +707,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isUsingCompression() {
|
public boolean isUsingCompression() {
|
||||||
return compressionHandler != null && serverAckdCompression;
|
return compressionHandler != null && serverAckdCompression;
|
||||||
}
|
}
|
||||||
|
@ -759,7 +767,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
*
|
*
|
||||||
* @throws IOException if there is an exception starting stream compression.
|
* @throws IOException if there is an exception starting stream compression.
|
||||||
*/
|
*/
|
||||||
void startStreamCompression() throws IOException {
|
private void startStreamCompression() throws IOException {
|
||||||
serverAckdCompression = true;
|
serverAckdCompression = true;
|
||||||
// Initialize the reader and writer with the new secured version
|
// Initialize the reader and writer with the new secured version
|
||||||
initReaderAndWriter();
|
initReaderAndWriter();
|
||||||
|
@ -776,7 +784,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
* Notifies the XMPP connection that stream compression negotiation is done so that the
|
* Notifies the XMPP connection that stream compression negotiation is done so that the
|
||||||
* connection process can proceed.
|
* connection process can proceed.
|
||||||
*/
|
*/
|
||||||
void streamCompressionNegotiationDone() {
|
private void streamCompressionNegotiationDone() {
|
||||||
synchronized (compressionLock) {
|
synchronized (compressionLock) {
|
||||||
compressionLock.notify();
|
compressionLock.notify();
|
||||||
}
|
}
|
||||||
|
@ -824,7 +832,7 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
*
|
*
|
||||||
* @param e the exception that causes the connection close event.
|
* @param e the exception that causes the connection close event.
|
||||||
*/
|
*/
|
||||||
synchronized void notifyConnectionError(Exception e) {
|
private synchronized void notifyConnectionError(Exception e) {
|
||||||
// Listeners were already notified of the exception, return right here.
|
// Listeners were already notified of the exception, return right here.
|
||||||
if ((packetReader == null || packetReader.done) &&
|
if ((packetReader == null || packetReader.done) &&
|
||||||
(packetWriter == null || packetWriter.done)) return;
|
(packetWriter == null || packetWriter.done)) return;
|
||||||
|
@ -836,61 +844,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
callConnectionClosedOnErrorListener(e);
|
callConnectionClosedOnErrorListener(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void processPacket(Packet packet) {
|
|
||||||
super.processPacket(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Reader getReader() {
|
|
||||||
return super.getReader();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Writer getWriter() {
|
|
||||||
return super.getWriter();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException {
|
|
||||||
super.throwConnectionExceptionOrNoResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setServiceName(String serviceName) {
|
|
||||||
super.setServiceName(serviceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void serverRequiresBinding() {
|
|
||||||
super.serverRequiresBinding();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setServiceCapsNode(String node) {
|
|
||||||
super.setServiceCapsNode(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void serverSupportsSession() {
|
|
||||||
super.serverSupportsSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setRosterVersioningSupported() {
|
|
||||||
super.setRosterVersioningSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void serverSupportsAccountCreation() {
|
|
||||||
super.serverSupportsAccountCreation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected SASLAuthentication getSASLAuthentication() {
|
|
||||||
return super.getSASLAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a notification indicating that the connection was reconnected successfully.
|
* Sends a notification indicating that the connection was reconnected successfully.
|
||||||
*/
|
*/
|
||||||
|
@ -907,4 +860,514 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected class PacketReader {
|
||||||
|
|
||||||
|
private Thread readerThread;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private volatile boolean done;
|
||||||
|
|
||||||
|
PacketReader() throws SmackException {
|
||||||
|
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 SmackException if the parser could not be reset.
|
||||||
|
*/
|
||||||
|
void init() throws SmackException {
|
||||||
|
done = false;
|
||||||
|
lastFeaturesParsed = false;
|
||||||
|
|
||||||
|
readerThread = new Thread() {
|
||||||
|
public void run() {
|
||||||
|
parsePackets(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
readerThread.setName("Smack Packet Reader (" + getConnectionCounter() + ")");
|
||||||
|
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.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
synchronized void startup() throws NoResponseException, IOException {
|
||||||
|
readerThread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait until either:
|
||||||
|
// - the servers last features stanza has been parsed
|
||||||
|
// - an exception is thrown while parsing
|
||||||
|
// - the timeout occurs
|
||||||
|
wait(getPacketReplyTimeout());
|
||||||
|
}
|
||||||
|
catch (InterruptedException ie) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
if (!lastFeaturesParsed) {
|
||||||
|
throwConnectionExceptionOrNoResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts the packet reader down. This method simply sets the 'done' flag to true.
|
||||||
|
*/
|
||||||
|
void shutdown() {
|
||||||
|
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 SmackException if the parser could not be reset.
|
||||||
|
*/
|
||||||
|
private void resetParser() throws SmackException {
|
||||||
|
try {
|
||||||
|
parser = XmlPullParserFactory.newInstance().newPullParser();
|
||||||
|
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||||||
|
parser.setInput(getReader());
|
||||||
|
}
|
||||||
|
catch (XmlPullParserException e) {
|
||||||
|
throw new SmackException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
String name = parser.getName();
|
||||||
|
ParsingExceptionCallback callback = getParsingExceptionCallback();
|
||||||
|
Packet packet;
|
||||||
|
try {
|
||||||
|
packet = PacketParserUtils.parseStanza(parser, XMPPTCPConnection.this);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String content = PacketParserUtils.parseContentDepth(parser, parserDepth);
|
||||||
|
UnparsablePacket message = new UnparsablePacket(content, e);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.handleUnparsablePacket(message);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (packet != null) {
|
||||||
|
processPacket(packet);
|
||||||
|
}
|
||||||
|
// We found an opening stream. Record information about it, then notify
|
||||||
|
// the connectionID lock so that the packet reader startup can finish.
|
||||||
|
else if (name.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);
|
||||||
|
}
|
||||||
|
else if (parser.getAttributeName(i).equals("from")) {
|
||||||
|
// Use the server name that the server says that it is.
|
||||||
|
setServiceName(parser.getAttributeValue(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (name.equals("error")) {
|
||||||
|
throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
|
||||||
|
}
|
||||||
|
else if (name.equals("features")) {
|
||||||
|
parseFeatures(parser);
|
||||||
|
}
|
||||||
|
else if (name.equals("proceed")) {
|
||||||
|
// Secure the connection by negotiating TLS
|
||||||
|
proceedTLSReceived();
|
||||||
|
// Reset the state of the parser since a new stream element is going
|
||||||
|
// to be sent by the server
|
||||||
|
resetParser();
|
||||||
|
}
|
||||||
|
else if (name.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
|
||||||
|
streamCompressionNegotiationDone();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// SASL authentication has failed. The server may close the connection
|
||||||
|
// depending on the number of retries
|
||||||
|
final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
|
||||||
|
processPacket(failure);
|
||||||
|
getSASLAuthentication().authenticationFailed(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (name.equals("challenge")) {
|
||||||
|
// The server is challenging the SASL authentication made by the client
|
||||||
|
String challengeData = parser.nextText();
|
||||||
|
processPacket(new Challenge(challengeData));
|
||||||
|
getSASLAuthentication().challengeReceived(challengeData);
|
||||||
|
}
|
||||||
|
else if (name.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
|
||||||
|
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
|
||||||
|
getSASLAuthentication().authenticated();
|
||||||
|
}
|
||||||
|
else if (name.equals("compressed")) {
|
||||||
|
// Server confirmed that it's possible to use stream compression. Start
|
||||||
|
// stream compression
|
||||||
|
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
|
||||||
|
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 || isSocketClosed())) {
|
||||||
|
synchronized(this) {
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
// Close the connection and notify connection listeners of the
|
||||||
|
// error.
|
||||||
|
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
|
||||||
|
getSASLAuthentication().setAvailableSASLMethods(
|
||||||
|
PacketParserUtils.parseMechanisms(parser));
|
||||||
|
}
|
||||||
|
else if (parser.getName().equals("bind")) {
|
||||||
|
// The server requires the client to bind a resource to the stream
|
||||||
|
serverRequiresBinding();
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
setServiceCapsNode(capsNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (parser.getName().equals("session")) {
|
||||||
|
// The server supports sessions
|
||||||
|
serverSupportsSession();
|
||||||
|
}
|
||||||
|
else if (parser.getName().equals("ver")) {
|
||||||
|
if (parser.getNamespace().equals("urn:xmpp:features:rosterver")) {
|
||||||
|
setRosterVersioningSupported();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (parser.getName().equals("compression")) {
|
||||||
|
// The server supports stream compression
|
||||||
|
setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser));
|
||||||
|
}
|
||||||
|
else if (parser.getName().equals("register")) {
|
||||||
|
serverSupportsAccountCreation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (eventType == XmlPullParser.END_TAG) {
|
||||||
|
if (parser.getName().equals("starttls")) {
|
||||||
|
// Confirm the server that we want to use TLS
|
||||||
|
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 (!isSecureConnection()) {
|
||||||
|
if (!startTLSReceived
|
||||||
|
&& 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
|
||||||
|
|| 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class PacketWriter {
|
||||||
|
public static final int QUEUE_SIZE = 500;
|
||||||
|
|
||||||
|
private final ArrayBlockingQueueWithShutdown<Packet> queue = new ArrayBlockingQueueWithShutdown<Packet>(QUEUE_SIZE, true);
|
||||||
|
|
||||||
|
private Thread writerThread;
|
||||||
|
private Writer writer;
|
||||||
|
|
||||||
|
private volatile boolean done;
|
||||||
|
|
||||||
|
protected AtomicBoolean shutdownDone = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new packet writer with the specified connection.
|
||||||
|
*/
|
||||||
|
PacketWriter() {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the writer in order to be used. It is called at the first connection and also
|
||||||
|
* is invoked if the connection is disconnected by an error.
|
||||||
|
*/
|
||||||
|
void init() {
|
||||||
|
writer = getWriter();
|
||||||
|
done = false;
|
||||||
|
shutdownDone.set(false);
|
||||||
|
|
||||||
|
queue.start();
|
||||||
|
writerThread = new Thread() {
|
||||||
|
public void run() {
|
||||||
|
writePackets(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
writerThread.setName("Smack Packet Writer (" + getConnectionCounter() + ")");
|
||||||
|
writerThread.setDaemon(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the specified packet to the server.
|
||||||
|
*
|
||||||
|
* @param packet the packet to send.
|
||||||
|
* @throws NotConnectedException
|
||||||
|
*/
|
||||||
|
public void sendPacket(Packet packet) throws NotConnectedException {
|
||||||
|
if (done) {
|
||||||
|
throw new NotConnectedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
queue.put(packet);
|
||||||
|
}
|
||||||
|
catch (InterruptedException ie) {
|
||||||
|
throw new NotConnectedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the packet writer thread and opens a connection to the server. The
|
||||||
|
* packet writer will continue writing packets until {@link #shutdown} or an
|
||||||
|
* error occurs.
|
||||||
|
*/
|
||||||
|
void startup() {
|
||||||
|
writerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setWriter(Writer writer) {
|
||||||
|
this.writer = writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the packet writer. Once this method has been called, no further
|
||||||
|
* packets will be written to the server.
|
||||||
|
*/
|
||||||
|
void shutdown() {
|
||||||
|
done = true;
|
||||||
|
queue.shutdown();
|
||||||
|
synchronized(shutdownDone) {
|
||||||
|
if (!shutdownDone.get()) {
|
||||||
|
try {
|
||||||
|
shutdownDone.wait(getPacketReplyTimeout());
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
LOGGER.log(Level.WARNING, "shutdown", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next available packet from the queue for writing.
|
||||||
|
*
|
||||||
|
* @return the next packet for writing.
|
||||||
|
*/
|
||||||
|
private Packet nextPacket() {
|
||||||
|
if (done) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Packet packet = null;
|
||||||
|
try {
|
||||||
|
packet = queue.take();
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writePackets(Thread thisThread) {
|
||||||
|
try {
|
||||||
|
// Open the stream.
|
||||||
|
openStream();
|
||||||
|
// Write out packets from the queue.
|
||||||
|
while (!done && (writerThread == thisThread)) {
|
||||||
|
Packet packet = nextPacket();
|
||||||
|
if (packet != null) {
|
||||||
|
writer.write(packet.toXML().toString());
|
||||||
|
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flush out the rest of the queue. If the queue is extremely large, it's possible
|
||||||
|
// we won't have time to entirely flush it before the socket is forced closed
|
||||||
|
// by the shutdown process.
|
||||||
|
try {
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
Packet packet = queue.remove();
|
||||||
|
writer.write(packet.toXML().toString());
|
||||||
|
}
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "Exception flushing queue during shutdown, ignore and continue", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the queue contents (hopefully nothing is left).
|
||||||
|
queue.clear();
|
||||||
|
|
||||||
|
// Close the stream.
|
||||||
|
try {
|
||||||
|
writer.write("</stream:stream>");
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
LOGGER.log(Level.WARNING, "Exception writing closing stream element", e);
|
||||||
|
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownDone.set(true);
|
||||||
|
synchronized(shutdownDone) {
|
||||||
|
shutdownDone.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ioe) {
|
||||||
|
// The exception can be ignored if the the connection is 'done'
|
||||||
|
// or if the it was caused because the socket got closed
|
||||||
|
if (!(done || isSocketClosed())) {
|
||||||
|
shutdown();
|
||||||
|
notifyConnectionError(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends to the server a new stream element. This operation may be requested several times
|
||||||
|
* so we need to encapsulate the logic in one place. This message will be sent while doing
|
||||||
|
* TLS, SASL and resource binding.
|
||||||
|
*
|
||||||
|
* @throws IOException If an error occurs while sending the stanza to the server.
|
||||||
|
*/
|
||||||
|
void openStream() throws IOException {
|
||||||
|
StringBuilder stream = new StringBuilder();
|
||||||
|
stream.append("<stream:stream");
|
||||||
|
stream.append(" to=\"").append(getServiceName()).append("\"");
|
||||||
|
stream.append(" xmlns=\"jabber:client\"");
|
||||||
|
stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
|
||||||
|
stream.append(" version=\"1.0\">");
|
||||||
|
writer.write(stream.toString());
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.concurrent.CyclicBarrier;
|
||||||
|
|
||||||
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
||||||
import org.jivesoftware.smack.packet.Message;
|
import org.jivesoftware.smack.packet.Message;
|
||||||
|
import org.jivesoftware.smack.tcp.XMPPTCPConnection.PacketWriter;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
@ -44,11 +45,11 @@ public class PacketWriterTest {
|
||||||
@Test
|
@Test
|
||||||
public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException {
|
public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException, NotConnectedException {
|
||||||
XMPPTCPConnection connection = new XMPPTCPConnection("foobar.com");
|
XMPPTCPConnection connection = new XMPPTCPConnection("foobar.com");
|
||||||
final PacketWriter pw = new PacketWriter(connection);
|
final PacketWriter pw = connection.new PacketWriter();
|
||||||
pw.setWriter(new BlockingStringWriter());
|
pw.setWriter(new BlockingStringWriter());
|
||||||
pw.startup();
|
pw.startup();
|
||||||
|
|
||||||
for (int i = 0; i < PacketWriter.QUEUE_SIZE; i++) {
|
for (int i = 0; i < XMPPTCPConnection.PacketWriter.QUEUE_SIZE; i++) {
|
||||||
pw.sendPacket(new Message());
|
pw.sendPacket(new Message());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue