Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token-based reconnection implementation #85

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions documentation/extensions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Currently supported XEPs of Smack (all subprojects)
| Name | XEP | Description |
|---------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| Nonzas | [XEP-0360](http://xmpp.org/extensions/xep-0360.html) | Defines the term "Nonza", describing every top level stream element that is not a Stanza. |
| Token-based reconnection | [XEP-xxxx](http://www.xmpp.org/extensions/inbox/token-reconnection.html) | Defines a token-based session authentication mechanism. |


Currently supported XEPs of smack-tcp
-------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,38 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import org.igniterealtime.jbosh.AbstractBody;
import org.igniterealtime.jbosh.BOSHClient;
import org.igniterealtime.jbosh.BOSHClientConfig;
import org.igniterealtime.jbosh.BOSHClientConnEvent;
import org.igniterealtime.jbosh.BOSHClientConnListener;
import org.igniterealtime.jbosh.BOSHClientRequestListener;
import org.igniterealtime.jbosh.BOSHClientResponseListener;
import org.igniterealtime.jbosh.BOSHException;
import org.igniterealtime.jbosh.BOSHMessageEvent;
import org.igniterealtime.jbosh.BodyQName;
import org.igniterealtime.jbosh.ComposableBody;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.ConnectionException;
import org.jivesoftware.smack.XMPPException.StreamErrorException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.StreamErrorException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.parts.Resourcepart;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import org.igniterealtime.jbosh.AbstractBody;
import org.igniterealtime.jbosh.BOSHClient;
import org.igniterealtime.jbosh.BOSHClientConfig;
import org.igniterealtime.jbosh.BOSHClientConnEvent;
import org.igniterealtime.jbosh.BOSHClientConnListener;
import org.igniterealtime.jbosh.BOSHClientRequestListener;
import org.igniterealtime.jbosh.BOSHClientResponseListener;
import org.igniterealtime.jbosh.BOSHException;
import org.igniterealtime.jbosh.BOSHMessageEvent;
import org.igniterealtime.jbosh.BodyQName;
import org.igniterealtime.jbosh.ComposableBody;

/**
* Creates a connection to an XMPP server via HTTP binding.
Expand Down Expand Up @@ -221,6 +222,20 @@ protected void loginInternal(String username, String password, Resourcepart reso
// Authenticate using SASL
saslAuthentication.authenticate(username, password, config.getAuthzid());

afterAuth(resource);
}

@Override
protected void loginInternal(String token, Resourcepart resource)
throws XMPPException, SmackException, IOException, InterruptedException {
// Authenticate using SASL
saslAuthentication.authenticate(token);

afterAuth(resource);
}

private void afterAuth(Resourcepart resource)
throws XMPPErrorException, SmackException, InterruptedException, NotConnectedException {
bindResourceAndEstablishSession(resource);

afterSuccessfulLogin(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@
import org.jivesoftware.smack.iqrequest.IQRequestHandler;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StartTls;
import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.StreamError;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
Expand Down Expand Up @@ -286,6 +286,11 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
private final Map<String, IQRequestHandler> setIqRequestHandler = new HashMap<>();
private final Map<String, IQRequestHandler> getIqRequestHandler = new HashMap<>();

/**
* Indicates the last refresh token received via the X-OAUTH mechanism.
*/
protected String refreshTokenXOAUTH;

/**
* Create a new XMPPConnection to an XMPP server.
*
Expand Down Expand Up @@ -384,13 +389,20 @@ public synchronized AbstractXMPPConnection connect() throws SmackException, IOEx
*/
protected abstract void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException;

private String usedUsername, usedPassword;
private String usedUsername, usedPassword, usedToken;

/**
* The resourcepart used for this connection. May not be the resulting resourcepart if it's null or overridden by the XMPP service.
*/
private Resourcepart usedResource;

/**
* Method to avoid reconnection using token.
*/
public void avoidTokenReconnection() {
usedToken = null;
}

/**
* Logs in to the server using the strongest SASL mechanism supported by
* the server. If more than the connection's default stanza(/packet) timeout elapses in each step of the
Expand Down Expand Up @@ -420,7 +432,12 @@ public synchronized void login() throws XMPPException, SmackException, IOExcepti
CharSequence username = usedUsername != null ? usedUsername : config.getUsername();
String password = usedPassword != null ? usedPassword : config.getPassword();
Resourcepart resource = usedResource != null ? usedResource : config.getResource();
login(username, password, resource);

if (usedToken != null) {
login(usedToken, resource);
} else {
login(username, password, resource);
}
}

/**
Expand Down Expand Up @@ -466,9 +483,35 @@ public synchronized void login(CharSequence username, String password, Resourcep
loginInternal(usedUsername, usedPassword, usedResource);
}

/**
* Login with the given token. Calling this method Smack will try to login
* using the X-OAUTH mechanism.
*
* @param token
* @param resource
* @throws XMPPException
* @throws SmackException
* @throws IOException
* @throws InterruptedException
*/
public synchronized void login(String token, Resourcepart resource)
throws XMPPException, SmackException, IOException, InterruptedException {

StringUtils.requireNotNullOrEmpty(token, "Token must not be null or empty");

throwNotConnectedExceptionIfAppropriate("Did you call connect() before login()?");
throwAlreadyLoggedInExceptionIfAppropriate();
usedToken = token;
usedResource = resource;
loginInternal(token, resource);
}

protected abstract void loginInternal(String username, String password, Resourcepart resource)
throws XMPPException, SmackException, IOException, InterruptedException;

protected abstract void loginInternal(String token, Resourcepart resource)
throws XMPPException, SmackException, IOException, InterruptedException;

@Override
public final boolean isConnected() {
return connected;
Expand Down Expand Up @@ -555,15 +598,34 @@ protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedEx
}
}

/**
* Get the X-OAUTH last refresh token.
*
* @return the X-OAUTH last refresh token
*/
@Override
public String getXOAUTHLastRefreshToken() {
return refreshTokenXOAUTH;
}

/**
* Set the X-OAUTH last refresh token.
*/
@Override
public void setXOAUTHLastRefreshToken(String token) {
refreshTokenXOAUTH = token;
}

@Override
public final boolean isAnonymous() {
return isAuthenticated() && SASLAnonymous.NAME.equals(getUsedSaslMechansism());
}

/**
* Get the name of the SASL mechanism that was used to authenticate this connection. This returns the name of
* mechanism which was used the last time this conneciton was authenticated, and will return <code>null</code> if
* this connection was not authenticated before.
* Get the name of the SASL mechanism that was used to authenticate this
* connection. This returns the name of mechanism which was used the last
* time this connection was authenticated, and will return <code>null</code>
* if this connection was not authenticated before.
*
* @return the name of the used SASL mechanism.
* @since 4.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,6 @@

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.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;

import javax.security.auth.callback.CallbackHandler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -40,6 +28,18 @@
import java.util.Set;
import java.util.logging.Logger;

import javax.security.auth.callback.CallbackHandler;

import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;

/**
* <p>This class is responsible authenticating the user using SASL, binding the resource
* to the connection and establishing a session with the server.</p>
Expand All @@ -63,6 +63,8 @@ public final class SASLAuthentication {

private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<SASLMechanism>();

private static SASLMechanism XOAUTH_REGISTERED_MECHANISM;

private static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<String>();

/**
Expand All @@ -77,6 +79,15 @@ public static void registerSASLMechanism(SASLMechanism mechanism) {
}
}

/**
* Register a new X-OAUTH SASL mechanism.
*
* @param mechanism
*/
public static void registerXOAUTHSASLMechanism(SASLMechanism mechanism) {
XOAUTH_REGISTERED_MECHANISM = mechanism;
}

/**
* Returns the registered SASLMechanism sorted by the level of preference.
*
Expand Down Expand Up @@ -203,13 +214,53 @@ public void authenticate(String username, String password, EntityBareJid authzid
}
}

if (saslException != null){
afterAuthenticate();
}

/**
* Authenticate with a given token.
*
* @param token
* @throws XMPPErrorException
* @throws SASLErrorException
* @throws IOException
* @throws SmackException
* @throws InterruptedException
*/
public void authenticate(String token)
throws XMPPErrorException, SASLErrorException, IOException, SmackException, InterruptedException {
currentMechanism = selectXOAUTHMechanism();

if (currentMechanism == null) {
return;
}

final CallbackHandler callbackHandler = configuration.getCallbackHandler();

synchronized (this) {
currentMechanism.authenticate(token, callbackHandler);

final long deadline = System.currentTimeMillis() + connection.getPacketReplyTimeout();
while (!authenticationSuccessful && saslException == null) {
final long now = System.currentTimeMillis();
if (now > deadline)
break;
// Wait until SASL negotiation finishes
wait(deadline - now);
}
}

afterAuthenticate();
}

private void afterAuthenticate() throws SmackException, SASLErrorException, NoResponseException {
if (saslException != null) {
if (saslException instanceof SmackException) {
throw (SmackException) saslException;
} else if (saslException instanceof SASLErrorException) {
throw (SASLErrorException) saslException;
} else {
throw new IllegalStateException("Unexpected exception type" , saslException);
throw new IllegalStateException("Unexpected exception type", saslException);
}
}

Expand All @@ -219,12 +270,13 @@ public void authenticate(String username, String password, EntityBareJid authzid
}

/**
* Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
* to <code>false</code>.
* Wrapper for {@link #challengeReceived(String, boolean)}, with
* <code>finalChallenge</code> set to <code>false</code>.
*
* @param challenge a base64 encoded string representing the challenge.
* @param challenge
* a base64 encoded string representing the challenge.
* @throws SmackException
* @throws InterruptedException
* @throws InterruptedException
*/
public void challengeReceived(String challenge) throws SmackException, InterruptedException {
challengeReceived(challenge, false);
Expand Down Expand Up @@ -314,6 +366,24 @@ String getNameOfLastUsedSaslMechansism() {
return lastUsedMech.getName();
}

private SASLMechanism selectXOAUTHMechanism() throws SmackException {
SASLMechanism mechanism = XOAUTH_REGISTERED_MECHANISM;

final List<String> serverMechanisms = getServerMechanisms();
if (serverMechanisms.isEmpty()) {
LOGGER.warning("Server did not report any SASL mechanisms");
}

String mechanismName = mechanism.getName();

if (serverMechanisms.contains(mechanismName)) {
return mechanism.instanceForAuthentication(connection);
} else {
return null;
}

}

private SASLMechanism selectMechanism(EntityBareJid authzid) throws SmackException {
Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
final List<String> serverMechanisms = getServerMechanisms();
Expand Down Expand Up @@ -345,6 +415,8 @@ private SASLMechanism selectMechanism(EntityBareJid authzid) throws SmackExcepti
}

synchronized (BLACKLISTED_MECHANISMS) {
List<SASLMechanism> mechanisms = REGISTERED_MECHANISMS;
mechanisms.add(XOAUTH_REGISTERED_MECHANISM);
// @formatter:off
throw new SmackException(
"No supported and enabled SASL Mechanism provided by server. " +
Expand Down
Loading