1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-10-04 21:49:33 +02:00

Add support for XEP-0198: Stream Management

- De-duplicate code by moving it into AbstractXMPPConnection
- Introduce TopLevelStreamElement as superclass for all XMPP stream elements.
- Add SynchronizationPoint, ParserUtils
- Add ParserUtils

Fixes SMACK-333 and SMACK-521
This commit is contained in:
Florian Schmaus 2014-09-11 09:49:16 +02:00
parent 07c10a7444
commit fc51f3df48
69 changed files with 3277 additions and 1083 deletions

View file

@ -33,7 +33,7 @@ allprojects {
// build, causing unnecessary rebuilds.
builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date())
oneLineDesc = 'An Open Source XMPP (Jabber) client library'
jxmppVersion = "0.2.0"
jxmppVersion = "0.3.0"
}
group = 'org.igniterealtime.smack'
sourceCompatibility = 1.7

View file

@ -9,6 +9,14 @@ for many of the protocol extensions.
This manual provides details about each of the "smackx" extensions, including
what it is, how to use it, and some simple example code.
Currently supported XEPs of smack-tcp
-------------------------------------
| Name | XEP | Description |
|---------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| [Stream Management](streammanagement.html) | [XEP-0198](http://xmpp.org/extensions/xep-0198.html) | Allows active management of an XML Stream between two XMPP entities (stanza acknowledgement, stream resumption). |
Smack Extensions and currently supported XEPs by Smack (smack-extensions)
-------------------------------------------------------------------------

View file

@ -0,0 +1,37 @@
Stream Management
=================
XMPPTCPConnection comes with support for Stream Management (SM).
**XEP related:** [XEP-0198](http://xmpp.org/extensions/xep-0198.html)
Known interoperability issues
-----------------------------
- SM resumption failes on prosody when compression in sync flush mode is used with prosody. See [Prosody issue #433](https://code.google.com/p/lxmppd/issues/detail?id=433).
Enabling stream management
------------------------
TODO
Getting notifications about acknowledges stanzas
------------------------------------------------
TODO
Requisting stanza acknowledgements from the server
--------------------------------------------------
### By using predicates
TODO
### Manually
TODO
Enable stream resumption
------------------------
TODO

25
resources/getCopyright.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
SCRIPTDIR="$(dirname ${BASH_SOURCE[0]})"
BASEDIR=${SCRIPTDIR}/..
cd $BASEDIR
SUBPROJECTS=$(grep -oP "\'.*\'" settings.gradle | sed "s;';;g")
for p in $SUBPROJECTS; do
echo "Copyright notices for $p"
# Find all .java files in the project
find $p/src -type f -name "*.java" -print0 | \
# Get the project string
xargs -0 grep -ohP '^.*\* Copyright \K.*' | \
# Sort the output
sort | \
# Remove duplicates
uniq | \
# Split multi Copyright statemtents, e.g. "2001-2013 FooBar, 2014 Baz"
tr ',' '\n' | \
# Remove whitespaces resulting from the previous split
sed "s/^[ \t]*//" | \
# Remove dots at the end and '©' at the beginning
sed "s/^© //" | sed "s/\.$//" | sed "s/^(C) //"
echo -ne "\n"
done

View file

@ -20,9 +20,8 @@ package org.jivesoftware.smack.bosh;
import java.io.StringReader;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Challenge;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.XMPPException.StreamErrorException;
import org.xmlpull.v1.XmlPullParserFactory;
@ -86,8 +85,6 @@ public class BOSHPacketReader implements BOSHClientResponseListener {
final String challengeData = parser.nextText();
connection.getSASLAuthentication()
.challengeReceived(challengeData);
connection.processPacket(new Challenge(
challengeData));
} else if (parser.getName().equals("success")) {
connection.send(ComposableBody.builder()
.setNamespaceDefinition("xmpp", XMPPBOSHConnection.XMPP_BOSH_NS)
@ -100,14 +97,12 @@ public class BOSHPacketReader implements BOSHClientResponseListener {
.build());
Success success = new Success(parser.nextText());
connection.getSASLAuthentication().authenticated(success);
connection.processPacket(success);
} else if (parser.getName().equals("features")) {
parseFeatures(parser);
} else if (parser.getName().equals("failure")) {
if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) {
final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
connection.getSASLAuthentication().authenticationFailed(failure);
connection.processPacket(failure);
}
} else if (parser.getName().equals("error")) {
throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
@ -123,41 +118,7 @@ public class BOSHPacketReader implements BOSHClientResponseListener {
}
}
/**
* Parse and setup the XML stream features.
*
* @param parser the XML parser, positioned at the start of a message packet.
* @throws Exception if an exception occurs while parsing the packet.
*/
private void parseFeatures(XmlPullParser parser) throws Exception {
boolean done = false;
while (!done) {
int eventType = parser.next();
if (eventType == XmlPullParser.START_TAG) {
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();
} else if (parser.getName().equals("session")) {
// The server supports sessions
connection.serverSupportsSession();
} else if (parser.getName().equals("register")) {
connection.serverSupportsAccountCreation();
}
} else if (eventType == XmlPullParser.END_TAG) {
if (parser.getName().equals("features")) {
done = true;
}
}
}
connection.parseFeatures0(parser);
}
}

View file

@ -38,9 +38,12 @@ import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PlainStreamElement;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Type;
import org.xmlpull.v1.XmlPullParser;
import org.igniterealtime.jbosh.BOSHClient;
import org.igniterealtime.jbosh.BOSHClientConfig;
import org.igniterealtime.jbosh.BOSHClientConnEvent;
@ -85,7 +88,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
// Some flags which provides some info about the current state.
private boolean connected = false;
private boolean authenticated = false;
private boolean anonymous = false;
private boolean isFirstInitialization = true;
private boolean wasAuthenticated = false;
private boolean done = false;
@ -104,11 +106,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
*/
protected String sessionID = null;
/**
* The full JID of the authenticated user.
*/
private String user = null;
/**
* Create a HTTP Binding connection to a XMPP server.
*
@ -220,10 +217,6 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
return user;
}
public boolean isAnonymous() {
return anonymous;
}
public boolean isAuthenticated() {
return authenticated;
}
@ -250,6 +243,10 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
if (authenticated) {
throw new AlreadyLoggedInException();
}
// Wait with SASL auth until the SASL mechanisms have been received
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
// Do partial version of nameprep on the username.
username = username.toLowerCase(Locale.US).trim();
@ -264,41 +261,11 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
throw new SaslException("No non-anonymous SASL authentication mechanism available");
}
String response = bindResourceAndEstablishSession(resource);
// Set the user.
if (response != null) {
this.user = response;
// Update the serviceName with the one returned by the server
setServiceName(response);
} else {
this.user = username + "@" + getServiceName();
if (resource != null) {
this.user += "/" + resource;
}
}
bindResourceAndEstablishSession(resource);
// Indicate that we're now authenticated.
authenticated = true;
anonymous = false;
// Stores the autentication for future reconnection
// Stores the authentication for future reconnection
setLoginInfo(username, password, resource);
// If debugging is enabled, change the the debug window title to include
// the
// name we are now logged-in as.l
if (config.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
callConnectionAuthenticatedListener();
// Set presence to online. It is important that this is done after
// callConnectionAuthenticatedListener(), as this call will also
// eventually load the roster. And we should load the roster before we
// send the initial presence.
if (config.isSendPresence()) {
sendPacket(new Presence(Presence.Type.available));
}
afterSuccessfulLogin(false, false);
}
public void loginAnonymously() throws XMPPException, SmackException, IOException {
@ -309,6 +276,9 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
throw new AlreadyLoggedInException();
}
// Wait with SASL auth until the SASL mechanisms have been received
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
if (saslAuthentication.hasAnonymousAuthentication()) {
saslAuthentication.authenticateAnonymously();
}
@ -317,38 +287,27 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
throw new SaslException("No anonymous SASL authentication mechanism available");
}
String response = bindResourceAndEstablishSession(null);
// Set the user value.
this.user = response;
// Update the serviceName with the one returned by the server
setServiceName(response);
bindResourceAndEstablishSession(null);
// Set presence to online.
if (config.isSendPresence()) {
sendPacket(new Presence(Presence.Type.available));
afterSuccessfulLogin(true, false);
}
@Override
public void send(PlainStreamElement element) throws NotConnectedException {
if (done) {
throw new NotConnectedException();
}
// Indicate that we're now authenticated.
authenticated = true;
anonymous = true;
// If debugging is enabled, change the the debug window title to include the
// name we are now logged-in as.
// If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
// will be null
if (config.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
callConnectionAuthenticatedListener();
sendElement(element);
}
@Override
protected void sendPacketInternal(Packet packet) throws NotConnectedException {
if (done) {
throw new NotConnectedException();
}
sendElement(packet);
}
private void sendElement(Element element) {
try {
send(ComposableBody.builder().setPayloadXML(packet.toXML().toString()).build());
send(ComposableBody.builder().setPayloadXML(element.toXML().toString()).build());
}
catch (BOSHException e) {
LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e);
@ -524,19 +483,8 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
return super.getSASLAuthentication();
}
@Override
protected void serverRequiresBinding() {
super.serverRequiresBinding();
}
@Override
protected void serverSupportsSession() {
super.serverSupportsSession();
}
@Override
protected void serverSupportsAccountCreation() {
super.serverSupportsAccountCreation();
void parseFeatures0(XmlPullParser parser) throws Exception {
parseFeatures(parser);
}
/**

View file

@ -20,6 +20,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -28,27 +29,43 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.ConnectionException;
import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException;
import org.jivesoftware.smack.SmackException.SecurityRequiredException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.compress.packet.Compress;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.filter.IQReplyFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.packet.CapsExtension;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smack.packet.RosterVer;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.PlainStreamElement;
import org.jivesoftware.smack.rosterstore.RosterStore;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jxmpp.util.XmppStringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public abstract class AbstractXMPPConnection implements XMPPConnection {
private static final Logger LOGGER = Logger.getLogger(AbstractXMPPConnection.class.getName());
@ -105,6 +122,15 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
protected final Map<PacketInterceptor, InterceptorWrapper> interceptors =
new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();
protected final Lock connectionLock = new ReentrantLock();
protected final Map<String, PacketExtension> streamFeatures = new HashMap<String, PacketExtension>();
/**
* The full JID of the authenticated user.
*/
protected String user;
/**
*
*/
@ -125,7 +151,21 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
protected Writer writer;
/**
* Set to success 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.
*/
protected final SynchronizationPoint<Exception> lastFeaturesReceived = new SynchronizationPoint<Exception>(
AbstractXMPPConnection.this);
/**
* Set to success if the sasl feature has been received.
*/
protected final SynchronizationPoint<SmackException> saslFeatureReceived = new SynchronizationPoint<SmackException>(
AbstractXMPPConnection.this);
/**
* The SASLAuthentication manager that is responsible for authenticating with the server.
*/
@ -142,21 +182,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
protected final ConnectionConfiguration config;
/**
* Holds the Caps Node information for the used XMPP service (i.e. the XMPP server)
*/
private String serviceCapsNode;
/**
* Defines how the from attribute of outgoing stanzas should be handled.
*/
private FromMode fromMode = FromMode.OMITTED;
/**
* Stores whether the server supports rosterVersioning
*/
private boolean rosterVersioningSupported = false;
protected XMPPInputOutputStream compressionHandler;
/**
@ -200,22 +230,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
protected int port;
/**
* Set to true if the server requires the connection to be binded in order to continue.
* <p>
* Note that we use AtomicBoolean here because it allows us to set the Boolean *object*, which
* we also use as synchronization object. A plain non-atomic Boolean object would be newly created
* for every change of the boolean value, which makes it useless as object for wait()/notify().
*/
private AtomicBoolean bindingRequired = new AtomicBoolean(false);
private boolean sessionSupported;
/**
*
*/
private Exception connectionException;
/**
* Flag that indicates if the user is currently authenticated with the server.
*/
@ -227,6 +241,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
protected boolean wasAuthenticated = false;
private boolean anonymous = false;
/**
* Create a new XMPPConnection to a XMPP server.
*
@ -267,14 +283,14 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
@Override
public abstract boolean isAuthenticated();
@Override
public abstract boolean isAnonymous();
@Override
public abstract boolean isSecureConnection();
protected abstract void sendPacketInternal(Packet packet) throws NotConnectedException;
@Override
public abstract void send(PlainStreamElement element) throws NotConnectedException;
@Override
public abstract boolean isUsingCompression();
@ -292,22 +308,16 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
public void connect() throws SmackException, IOException, XMPPException {
saslAuthentication.init();
bindingRequired.set(false);
sessionSupported = false;
connectionException = null;
saslFeatureReceived.init();
lastFeaturesReceived.init();
connectInternal();
}
/**
* Abstract method that concrete subclasses of XMPPConnection need to implement to perform their
* way of XMPP connection establishment. Implementations must guarantee that this method will
* block until the last features stanzas has been parsed and the features have been reported
* back to XMPPConnection (e.g. by calling @{link {@link AbstractXMPPConnection#serverRequiresBinding()}
* and such).
* <p>
* Also implementations are required to perform an automatic login if the previous connection
* state was logged (authenticated).
*
* way of XMPP connection establishment. Implementations are required to perform an automatic
* login if the previous connection state was logged (authenticated).
*
* @throws SmackException
* @throws IOException
* @throws XMPPException
@ -383,44 +393,20 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
*/
public abstract void loginAnonymously() throws XMPPException, SmackException, IOException;
/**
* Notification message saying that the server requires the client to bind a
* resource to the stream.
*/
protected void serverRequiresBinding() {
synchronized (bindingRequired) {
bindingRequired.set(true);
bindingRequired.notify();
}
}
protected void bindResourceAndEstablishSession(String resource) throws XMPPErrorException,
IOException, SmackException {
/**
* Notification message saying that the server supports sessions. When a server supports
* sessions the client needs to send a Session packet after successfully binding a resource
* for the session.
*/
protected void serverSupportsSession() {
sessionSupported = true;
}
// Wait until either:
// - the servers last features stanza has been parsed
// - the timeout occurs
LOGGER.finer("Waiting for last features to be received before continuing with resource binding");
lastFeaturesReceived.checkIfSuccessOrWait();
protected String bindResourceAndEstablishSession(String resource) throws XMPPErrorException,
ResourceBindingNotOfferedException, NoResponseException, NotConnectedException {
synchronized (bindingRequired) {
if (!bindingRequired.get()) {
try {
bindingRequired.wait(getPacketReplyTimeout());
}
catch (InterruptedException e) {
// Ignore
}
if (!bindingRequired.get()) {
// Server never offered resource binding, which is REQURIED in XMPP client and
// server
// implementations as per RFC6120 7.2
throw new ResourceBindingNotOfferedException();
}
}
if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) {
// Server never offered resource binding, which is REQURIED in XMPP client and
// server implementations as per RFC6120 7.2
throw new ResourceBindingNotOfferedException();
}
// Resource binding, see RFC6120 7.
@ -435,9 +421,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
throw e;
}
Bind response = packetCollector.nextResultOrThrow();
String userJID = response.getJid();
user = response.getJid();
setServiceName(XmppStringUtils.parseDomain(user));
if (sessionSupported && !getConfiguration().isLegacySessionDisabled()) {
if (hasFeature(Session.ELEMENT, Session.NAMESPACE) && !getConfiguration().isLegacySessionDisabled()) {
Session session = new Session();
packetCollector = createPacketCollector(new PacketIDFilter(session));
try {
@ -448,58 +435,59 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
}
packetCollector.nextResultOrThrow();
}
return userJID;
}
protected void setConnectionException(Exception e) {
connectionException = e;
}
protected void afterSuccessfulLogin(final boolean anonymous, final boolean resumed) throws NotConnectedException {
// Indicate that we're now authenticated.
this.authenticated = true;
this.anonymous = anonymous;
protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException, SmackException {
if (connectionException != null) {
if (connectionException instanceof IOException) {
throw (IOException) connectionException;
} else if (connectionException instanceof SmackException) {
throw (SmackException) connectionException;
} else {
throw new SmackException(connectionException);
}
} else {
throw new NoResponseException();
// If debugging is enabled, change the the debug window title to include the
// name we are now logged-in as.
// If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
// will be null
if (config.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
callConnectionAuthenticatedListener();
// Set presence to online. It is important that this is done after
// callConnectionAuthenticatedListener(), as this call will also
// eventually load the roster. And we should load the roster before we
// send the initial presence.
if (config.isSendPresence() && !resumed) {
sendPacket(new Presence(Presence.Type.available));
}
}
protected Reader getReader() {
return reader;
}
protected Writer getWriter() {
return writer;
@Override
public boolean isAnonymous() {
return anonymous;
}
protected void setServiceName(String serviceName) {
config.setServiceName(serviceName);
}
protected void setLoginInfo(String username, String password, String resource) {
config.setLoginInfo(username, password, resource);
}
protected void serverSupportsAccountCreation() {
AccountManager.getInstance(this).setSupportsAccountCreation(true);
}
protected void maybeResolveDns() throws Exception {
config.maybeResolveDns();
}
protected Lock getConnectionLock() {
return connectionLock;
}
@Override
public void sendPacket(Packet packet) throws NotConnectedException {
if (!isConnected()) {
throw new NotConnectedException();
}
if (packet == null) {
throw new NullPointerException("Packet is null.");
throw new IllegalArgumentException("Packet must not be null");
}
switch (fromMode) {
case OMITTED:
@ -800,35 +788,6 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
}
}
/**
* Set the servers Entity Caps node
*
* XMPPConnection holds this information in order to avoid a dependency to
* smack-extensions where EntityCapsManager lives from smack.
*
* @param node
*/
protected void setServiceCapsNode(String node) {
serviceCapsNode = node;
}
@Override
public String getServiceCapsNode() {
return serviceCapsNode;
}
@Override
public boolean isRosterVersioningSupported() {
return rosterVersioningSupported;
}
/**
* Indicates that the server supports roster versioning as defined in XEP-0237.
*/
protected void setRosterVersioningSupported() {
rosterVersioningSupported = true;
}
@Override
public long getPacketReplyTimeout() {
return packetReplyTimeout;
@ -1055,6 +1014,112 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
return config.isRosterLoadedAtLogin();
}
protected final void parseFeatures(XmlPullParser parser) throws XmlPullParserException,
IOException, SecurityRequiredException, NotConnectedException {
streamFeatures.clear();
final int initialDepth = parser.getDepth();
while (true) {
int eventType = parser.next();
if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initialDepth + 1) {
String name = parser.getName();
String namespace = parser.getNamespace();
switch (name) {
case StartTls.ELEMENT:
StartTls startTls = PacketParserUtils.parseStartTlsFeature(parser);
addStreamFeature(startTls);
break;
case Mechanisms.ELEMENT:
Mechanisms mechanisms = new Mechanisms(PacketParserUtils.parseMechanisms(parser));
addStreamFeature(mechanisms);
break;
case Bind.ELEMENT:
addStreamFeature(Bind.Feature.INSTANCE);
break;
case CapsExtension.ELEMENT:
// Set the entity caps node for the server if one is send
// See http://xmpp.org/extensions/xep-0115.html#stream
String node = parser.getAttributeValue(null, "node");
String ver = parser.getAttributeValue(null, "ver");
String hash = parser.getAttributeValue(null, "hash");
CapsExtension capsExtension = new CapsExtension(node, ver, hash);
addStreamFeature(capsExtension);
break;
case Session.ELEMENT:
addStreamFeature(Session.Feature.INSTANCE);
break;
case RosterVer.ELEMENT:
if(namespace.equals(RosterVer.NAMESPACE)) {
addStreamFeature(RosterVer.INSTANCE);
}
else {
LOGGER.severe("Unkown Roster Versioning Namespace: "
+ namespace
+ ". Roster versioning not enabled");
}
break;
case Compress.Feature.ELEMENT:
Compress.Feature compression = PacketParserUtils.parseCompressionFeature(parser);
addStreamFeature(compression);
break;
case Registration.Feature.ELEMENT:
addStreamFeature(Registration.Feature.INSTANCE);
break;
default:
parseFeaturesSubclass(name, namespace, parser);
break;
}
}
else if (eventType == XmlPullParser.END_TAG && parser.getDepth() == initialDepth) {
break;
}
}
if (hasFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE)) {
// Only proceed with SASL auth if TLS is disabled or if the server doesn't announce it
if (!hasFeature(StartTls.ELEMENT, StartTls.NAMESPACE)
|| config.getSecurityMode() == SecurityMode.disabled) {
saslFeatureReceived.reportSuccess();
}
}
// If the server reported the bind feature then we are that that we did SASL and maybe
// STARTTLS. We can then report that the last 'stream:features' have been parsed
if (hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) {
if (!hasFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE)
|| !config.isCompressionEnabled()) {
// This was was last features from the server is either it did not contain
// compression or if we disabled it
lastFeaturesReceived.reportSuccess();
}
}
afterFeaturesReceived();
}
protected void parseFeaturesSubclass (String name, String namespace, XmlPullParser parser) {
// Default implementation does nothing
}
protected void afterFeaturesReceived() throws SecurityRequiredException, NotConnectedException {
// Default implementation does nothing
}
@SuppressWarnings("unchecked")
@Override
public <F extends PacketExtension> F getFeature(String element, String namespace) {
return (F) streamFeatures.get(XmppStringUtils.generateKey(element, namespace));
}
@Override
public boolean hasFeature(String element, String namespace) {
return getFeature(element, namespace) != null;
}
protected void addStreamFeature(PacketExtension feature) {
String key = XmppStringUtils.generateKey(feature.getElementName(), feature.getNamespace());
streamFeatures.put(key, feature);
}
private final ScheduledExecutorService removeCallbacksService = new ScheduledThreadPoolExecutor(1,
new SmackExecutorThreadFactory(connectionCounterValue));

View file

@ -37,7 +37,6 @@ import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.IQReplyFilter;
import org.jivesoftware.smack.filter.IQTypeFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
@ -45,6 +44,7 @@ import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.RosterPacket;
import org.jivesoftware.smack.packet.RosterVer;
import org.jivesoftware.smack.packet.RosterPacket.Item;
import org.jivesoftware.smack.rosterstore.RosterStore;
import org.jxmpp.util.XmppStringUtils;
@ -227,12 +227,15 @@ public class Roster {
}
RosterPacket packet = new RosterPacket();
if (rosterStore != null && connection.isRosterVersioningSupported()) {
if (rosterStore != null && isRosterVersioningSupported()) {
packet.setVersion(rosterStore.getRosterVersion());
}
PacketFilter filter = new IQReplyFilter(packet, connection);
connection.addPacketListener(new RosterResultListener(), filter);
connection.sendPacket(packet);
connection.sendIqWithResponseCallback(packet, new RosterResultListener(), new ExceptionCallback() {
@Override
public void processException(Exception exception) {
LOGGER.log(Level.SEVERE, "Exception reloading roster" , exception);
}
});
}
/**
@ -791,6 +794,10 @@ public class Roster {
|| item.getItemType().equals(RosterPacket.ItemType.both);
}
private boolean isRosterVersioningSupported() {
return connection.hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE);
}
/**
* An enumeration for the subscription mode options.
*/
@ -939,23 +946,13 @@ public class Roster {
}
/**
* Handles the case of the empty IQ-result for roster versioning.
*
* Intended to listen for a concrete roster result and deregisters
* itself after a processed packet.
* Handles roster reults as described in RFC 6121 2.1.4
*/
private class RosterResultListener implements PacketListener {
@Override
public void processPacket(Packet packet) {
connection.removePacketListener(this);
IQ result = (IQ)packet;
if (!result.getType().equals(IQ.Type.result)) {
LOGGER.severe("Roster result IQ not of type result. Packet: " + result.toXML());
return;
}
LOGGER.fine("RosterResultListener received stanza");
Collection<String> addedEntries = new ArrayList<String>();
Collection<String> updatedEntries = new ArrayList<String>();
Collection<String> deletedEntries = new ArrayList<String>();

View file

@ -19,17 +19,17 @@ package org.jivesoftware.smack;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.sasl.SASLAnonymous;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import javax.security.auth.callback.CallbackHandler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -128,13 +128,12 @@ public class SASLAuthentication {
}
private final AbstractXMPPConnection connection;
private Collection<String> serverMechanisms = new ArrayList<String>();
private SASLMechanism currentMechanism = null;
/**
* Boolean indicating if SASL negotiation has finished and was successful.
*/
private boolean saslNegotiated;
private boolean authenticationSuccessful;
/**
* Either of type {@link SmackException} or {@link SASLErrorException}
@ -152,7 +151,7 @@ public class SASLAuthentication {
* @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
*/
public boolean hasAnonymousAuthentication() {
return serverMechanisms.contains("ANONYMOUS");
return serverMechanisms().contains("ANONYMOUS");
}
/**
@ -161,7 +160,7 @@ public class SASLAuthentication {
* @return true if the server offered SASL authentication besides ANONYMOUS SASL.
*/
public boolean hasNonAnonymousAuthentication() {
return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
return !serverMechanisms().isEmpty() && (serverMechanisms().size() != 1 || !hasAnonymousAuthentication());
}
/**
@ -197,7 +196,7 @@ public class SASLAuthentication {
maybeThrowException();
if (!saslNegotiated) {
if (!authenticationSuccessful) {
throw new NoResponseException();
}
}
@ -244,7 +243,7 @@ public class SASLAuthentication {
maybeThrowException();
if (!saslNegotiated) {
if (!authenticationSuccessful) {
throw new NoResponseException();
}
}
@ -283,7 +282,7 @@ public class SASLAuthentication {
maybeThrowException();
if (!saslNegotiated) {
if (!authenticationSuccessful) {
throw new NoResponseException();
}
}
@ -299,17 +298,6 @@ public class SASLAuthentication {
}
}
}
/**
* Sets the available SASL mechanism reported by the server. The server will report the
* available SASL mechanism once the TLS negotiation was successful. This information is
* stored and will be used when doing the authentication for logging in the user.
*
* @param mechanisms collection of strings with the available SASL mechanism reported
* by the server.
*/
public void setAvailableSASLMethods(Collection<String> mechanisms) {
this.serverMechanisms = mechanisms;
}
/**
* Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
@ -355,7 +343,7 @@ public class SASLAuthentication {
if (success.getData() != null) {
challengeReceived(success.getData(), true);
}
saslNegotiated = true;
authenticationSuccessful = true;
// Wake up the thread that is waiting in the #authenticate method
synchronized (this) {
notify();
@ -381,13 +369,17 @@ public class SASLAuthentication {
}
}
public boolean authenticationSuccessful() {
return authenticationSuccessful;
}
/**
* Initializes the internal state in order to be able to be reused. The authentication
* is used by the connection at the first login and then reused after the connection
* is disconnected and then reconnected.
*/
protected void init() {
saslNegotiated = false;
authenticationSuccessful = false;
saslException = null;
}
@ -404,7 +396,7 @@ public class SASLAuthentication {
continue;
}
}
if (serverMechanisms.contains(mechanismName)) {
if (serverMechanisms().contains(mechanismName)) {
// Create a new instance of the SASLMechanism for every authentication attempt.
selectedMechanism = mechanism.instanceForAuthentication(connection);
break;
@ -412,4 +404,9 @@ public class SASLAuthentication {
}
return selectedMechanism;
}
private List<String> serverMechanisms() {
Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE);
return mechanisms.getMechanisms();
}
}

View file

@ -89,6 +89,17 @@ public class SmackException extends Exception {
}
}
public static class AlreadyConnectedException extends SmackException {
/**
*
*/
private static final long serialVersionUID = 5011416918049135231L;
public AlreadyConnectedException() {
}
}
public static class NotConnectedException extends SmackException {
/**
@ -120,6 +131,10 @@ public class SmackException extends Exception {
public SecurityRequiredException() {
}
public SecurityRequiredException(String message) {
super(message);
}
}
public static class SecurityNotPossibleException extends SmackException {

View file

@ -0,0 +1,190 @@
/**
*
* Copyright © 2014 Florian Schmaus
*
* 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.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.packet.TopLevelStreamElement;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PlainStreamElement;
public class SynchronizationPoint<E extends Exception> {
private static final Logger LOGGER = Logger.getLogger(SynchronizationPoint.class.getName());
private final AbstractXMPPConnection connection;
private final Lock connectionLock;
private final Condition condition;
private State state;
private E failureException;
public SynchronizationPoint(AbstractXMPPConnection connection) {
this.connection = connection;
this.connectionLock = connection.getConnectionLock();
this.condition = connection.getConnectionLock().newCondition();
init();
}
public void init() {
state = State.Initial;
failureException = null;
}
public void sendAndWaitForResponse(TopLevelStreamElement request) throws NoResponseException,
NotConnectedException {
assert (state == State.Initial);
connectionLock.lock();
try {
if (request != null) {
if (request instanceof Packet) {
connection.sendPacket((Packet) request);
}
else if (request instanceof PlainStreamElement){
connection.send((PlainStreamElement) request);
} else {
throw new IllegalStateException("Unsupported element type");
}
state = State.RequestSent;
}
waitForConditionOrTimeout();
}
finally {
connectionLock.unlock();
}
checkForResponse();
}
public void sendAndWaitForResponseOrThrow(PlainStreamElement request) throws E, NoResponseException,
NotConnectedException {
sendAndWaitForResponse(request);
switch (state) {
case Failure:
if (failureException != null) {
throw failureException;
}
break;
default:
// Success, do nothing
}
}
public void checkIfSuccessOrWaitOrThrow() throws NoResponseException, E {
checkIfSuccessOrWait();
if (state == State.Failure) {
throw failureException;
}
}
public void checkIfSuccessOrWait() throws NoResponseException {
connectionLock.lock();
try {
if (state == State.Success) {
// Return immediately
return;
}
waitForConditionOrTimeout();
} finally {
connectionLock.unlock();
}
checkForResponse();
}