/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright 2003-2007 Jive Software.
*
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.sasl;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.apache.harmony.javax.security.auth.callback.NameCallback;
import org.apache.harmony.javax.security.sasl.RealmCallback;
import org.apache.harmony.javax.security.sasl.RealmChoiceCallback;
import org.apache.harmony.javax.security.sasl.Sasl;
import org.apache.harmony.javax.security.sasl.SaslClient;
import org.apache.harmony.javax.security.sasl.SaslException;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.Base64;
/**
* Base class for SASL mechanisms. Subclasses must implement these methods:
* <ul>
* <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
* </ul>
* Subclasses will likely want to implement their own versions of these mthods:
* <li>{@link #authenticate(String, String, String)} -- Initiate authentication
* stanza using the deprecated method.</li> <li>
* {@link #authenticate(String, String, CallbackHandler)} -- Initiate
* authentication stanza using the CallbackHandler method.</li> <li>
* {@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
* </ul>
*
* @author Jay Kline
*/
public abstract class SASLMechanism implements CallbackHandler {
/**
* Initiating SASL authentication by select a mechanism.
*/
public class AuthMechanism extends Packet {
final private String name;
final private String authenticationText;
public AuthMechanism(String name, String authenticationText) {
if (name == null) {
throw new NullPointerException(
"SASL mechanism name shouldn't be null.");
}
this.name = name;
this.authenticationText = authenticationText;
}
@Override
public String toXML() {
final StringBuilder stanza = new StringBuilder();
stanza.append("<auth mechanism=\"").append(name);
stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null
&& authenticationText.trim().length() > 0) {
stanza.append(authenticationText);
}
stanza.append("</auth>");
return stanza.toString();
}
}
/**
* A SASL challenge stanza.
*/
public static class Challenge extends Packet {
final private String data;
public Challenge(String data) {
this.data = data;
}
@Override
public String toXML() {
final StringBuilder stanza = new StringBuilder();
stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null && data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</challenge>");
return stanza.toString();
}
}
/**
* A SASL failure stanza.
*/
public static class Failure extends Packet {
final private String condition;
public Failure(String condition) {
this.condition = condition;
}
/**
* Get the SASL related error condition.
*
* @return the SASL related error condition.
*/
public String getCondition() {
return condition;
}
@Override
public String toXML() {
final StringBuilder stanza = new StringBuilder();
stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (condition != null && condition.trim().length() > 0) {
stanza.append("<").append(condition).append("/>");
}
stanza.append("</failure>");
return stanza.toString();
}
}
/**
* A SASL response stanza.
*/
public class Response extends Packet {
final private String authenticationText;
public Response() {
authenticationText = null;
}
public Response(String authenticationText) {
if (authenticationText == null
|| authenticationText.trim().length() == 0) {
this.authenticationText = null;
} else {
this.authenticationText = authenticationText;
}
}
@Override
public String toXML() {
final StringBuilder stanza = new StringBuilder();
stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (authenticationText != null) {
stanza.append(authenticationText);
}
stanza.append("</response>");
return stanza.toString();
}
}
/**
* A SASL success stanza.
*/
public static class Success extends Packet {
final private String data;
public Success(String data) {
this.data = data;
}
@Override
public String toXML() {
final StringBuilder stanza = new StringBuilder();
stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
if (data != null && data.trim().length() > 0) {
stanza.append(data);
}
stanza.append("</success>");
return stanza.toString();
}
}
private final SASLAuthentication saslAuthentication;
protected SaslClient sc;
protected String authenticationId;
protected String password;
protected String hostname;
public SASLMechanism(SASLAuthentication saslAuthentication) {
this.saslAuthentication = saslAuthentication;
}
protected void authenticate() throws IOException, XMPPException {
String authenticationText = null;
try {
if (sc.hasInitialResponse()) {
final byte[] response = sc.evaluateChallenge(new byte[0]);
authenticationText = Base64.encodeBytes(response,
Base64.DONT_BREAK_LINES);
}
} catch (final SaslException e) {
throw new XMPPException("SASL authentication failed", e);
}
// Send the authentication to the server
getSASLAuthentication().send(
new AuthMechanism(getName(), authenticationText));
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server. The callback
* handler will handle any additional information, such as the
* authentication ID or realm, if it is needed.
*
* @param username
* the username of the user being authenticated.
* @param host
* the hostname where the user account resides.
* @param cbh
* the CallbackHandler to obtain user information.
* @throws IOException
* If a network error occures while authenticating.
* @throws XMPPException
* If a protocol error occurs or the user is not authenticated.
*/
public void authenticate(String username, String host, CallbackHandler cbh)
throws IOException, XMPPException {
final String[] mechanisms = { getName() };
final Map<String, String> props = new HashMap<String, String>();
sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props,
cbh);
authenticate();
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server. Note that this
* method of authentication is not recommended, since it is very inflexable.
* Use {@link #authenticate(String, String, CallbackHandler)} whenever
* possible.
*
* @param username
* the username of the user being authenticated.
* @param host
* the hostname where the user account resides.
* @param password
* the password for this account.
* @throws IOException
* If a network error occurs while authenticating.
* @throws XMPPException
* If a protocol error occurs or the user is not authenticated.
*/
public void authenticate(String username, String host, String password)
throws IOException, XMPPException {
// Since we were not provided with a CallbackHandler, we will use our
// own with the given
// information
// Set the authenticationID as the username, since they must be the same
// in this case.
authenticationId = username;
this.password = password;
hostname = host;
final String[] mechanisms = { getName() };
final Map<String, String> props = new HashMap<String, String>();
sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props,
this);
authenticate();
}
/**
* The server is challenging the SASL mechanism for the stanza he just sent.
* Send a response to the server's challenge.
*
* @param challenge
* a base64 encoded string representing the challenge.
* @throws IOException
* if an exception sending the response occurs.
*/
public void challengeReceived(String challenge) throws IOException {
byte response[];
if (challenge != null) {
response = sc.evaluateChallenge(Base64.decode(challenge));
} else {
response = sc.evaluateChallenge(new byte[0]);
}
Packet responseStanza;
if (response == null) {
responseStanza = new Response();
} else {
responseStanza = new Response(Base64.encodeBytes(response,
Base64.DONT_BREAK_LINES));
}
// Send the authentication to the server
getSASLAuthentication().send(responseStanza);
}
/**
* Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
* GSSAPI.
*
* @return the common name of the SASL mechanism.
*/
protected abstract String getName();
protected SASLAuthentication getSASLAuthentication() {
return saslAuthentication;
}
/**
*
*/
@Override
public void handle(Callback[] callbacks) throws IOException,
UnsupportedCallbackException {
for (final Callback callback : callbacks) {
if (callback instanceof NameCallback) {
final NameCallback ncb = (NameCallback) callback;
ncb.setName(authenticationId);
} else if (callback instanceof PasswordCallback) {
final PasswordCallback pcb = (PasswordCallback) callback;
pcb.setPassword(password.toCharArray());
} else if (callback instanceof RealmCallback) {
final RealmCallback rcb = (RealmCallback) callback;
rcb.setText(hostname);
} else if (callback instanceof RealmChoiceCallback) {
// unused
// RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
} else {
throw new UnsupportedCallbackException(callback);
}
}
}
}