Add smack-android and redesign SASL authentication

This commit marks an important milestone with the addition of the
smack-android subproject. Smack is now able to run native on Android
without requiring any modifications, which makes the aSmack build
environment obsolete.

It was necessary to redesign the code for SASL authentication to achieve
this. Smack now comes with smack-sasl-provided for SASL implementations
that do not rely on additional APIs like javax for platforms where those
APIs are not available like Android.
filetransferTypos
Florian Schmaus 8 years ago
parent 5a2149718a
commit 89dc3a0e85
  1. 11
      .travis.yml
  2. 2
      build.gradle
  3. 5
      settings.gradle
  4. 50
      smack-android/build.gradle
  5. 174
      smack-android/src/main/java/org/jivesoftware/smackx/debugger/android/AndroidDebugger.java
  6. 11
      smack-bosh/src/main/java/org/jivesoftware/smack/bosh/BOSHPacketReader.java
  7. 15
      smack-core/build.gradle
  8. 14
      smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java
  9. 364
      smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java
  10. 2
      smack-core/src/main/java/org/jivesoftware/smack/SmackConfiguration.java
  11. 37
      smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java
  12. 2
      smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLErrorException.java
  13. 89
      smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java
  14. 319
      smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java
  15. 186
      smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStanzas.java
  16. 48
      smack-core/src/main/java/org/jivesoftware/smack/util/ByteUtils.java
  17. 2
      smack-core/src/main/java/org/jivesoftware/smack/util/PacketParserUtils.java
  18. 68
      smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java
  19. 2
      smack-core/src/main/resources/org.jivesoftware.smack/smack-config.xml
  20. 31
      smack-core/src/test/java/org/jivesoftware/smack/sasl/AbstractSaslTest.java
  21. 61
      smack-core/src/test/java/org/jivesoftware/smack/sasl/DigestMd5SaslTest.java
  22. 3
      smack-debug/src/main/java/org/jivesoftware/smackx/debugger/LiteDebugger.java
  23. 1
      smack-java7/build.gradle
  24. 8
      smack-sasl-javax/build.gradle
  25. 22
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLCramMD5Mechanism.java
  26. 22
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Mechanism.java
  27. 23
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLExternalMechanism.java
  28. 69
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLGSSAPIMechanism.java
  29. 147
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXMechanism.java
  30. 42
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLJavaXSmackInitializer.java
  31. 22
      smack-sasl-javax/src/main/java/org/jivesoftware/smack/sasl/javax/SASLPlainMechanism.java
  32. 6
      smack-sasl-javax/src/main/resources/org.jivesoftware.smack/smack-sasl-javax-components.xml
  33. 34
      smack-sasl-javax/src/test/java/org/jivesoftware/smack/sasl/javax/SASLDigestMD5Test.java
  34. 8
      smack-sasl-provided/build.gradle
  35. 206
      smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Mechanism.java
  36. 37
      smack-sasl-provided/src/main/java/org/jivesoftware/smack/sasl/provided/SASLProvidedSmackInitializer.java
  37. 6
      smack-sasl-provided/src/main/resources/org.jivesoftware.smack/smack-sasl-provided-components.xml
  38. 34
      smack-sasl-provided/src/test/java/org/jivesoftware/smack/sasl/provided/SASLDigestMD5Test.java
  39. 20
      smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java

@ -1 +1,10 @@
language: java
language: java
before_install:
- sudo apt-get update -qq
- if [ `uname -m` = x86_64 ]; then sudo apt-get install -qq --force-yes libgd2-xpm ia32-libs ia32-libs-multiarch; fi
- wget http://dl.google.com/android/android-sdk_r23.0.2-linux.tgz
- tar xzf android-*
- export ANDROID_HOME=$PWD/android-sdk-linux
- export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
- echo "y" | android update sdk --filter platform-tools,android-8 --no-ui --force

@ -237,7 +237,7 @@ subprojects {
}
}
['smack-resolver-javax'].each { name ->
['smack-resolver-javax', 'smack-sasl-javax', 'smack-sasl-provided'].each { name ->
project(":$name") {
jar {
manifest {

@ -6,8 +6,11 @@ include 'smack-core',
'smack-resolver-dnsjava',
'smack-resolver-minidns',
'smack-resolver-javax',
'smack-sasl-javax',
'smack-sasl-provided',
'smack-compression-jzlib',
'smack-legacy',
'smack-jingle',
'smack-bosh',
'smack-java7'
'smack-android',
'smack-java7'

@ -0,0 +1,50 @@
description = """\
Smack for Android.
All the required dependencies to run Smack on Android"""
ext {
smackMinAndroidSdk = 8
androidProjects = [':smack-tcp',':smack-core', ':smack-resolver-minidns', ':smack-sasl-provided', ':smack-extensions', ':smack-experimental'].collect{ project(it) }
}
// Note that the test dependencies (junit, ) are inferred from the
// sourceSet.test of the core subproject
dependencies {
androidProjects.each { project ->
compile project
}
}
def getAndroidRuntimeJar() {
def androidHome = new File("$System.env.ANDROID_HOME")
if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set")
new File("$androidHome/platforms/android-$smackMinAndroidSdk/android.jar")
}
def getAndroidJavadocOffline() {
def androidHome = new File("$System.env.ANDROID_HOME")
if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set")
return "$System.env.ANDROID_HOME" + "/docs/reference"
}
compileJava {
options.bootClasspath = getAndroidRuntimeJar()
}
// See http://stackoverflow.com/a/2823592/194894
// TODO this doesn't seem to work right now. But on the other hand it
// is not really required, just to avoid a javadoc compiler warning
javadoc {
options.linksOffline "http://developer.android.com/reference", getAndroidJavadocOffline()
}
configure (androidProjects) {
task compileAndroid(type: JavaCompile) {
source = compileJava.source
classpath = compileJava.classpath
destinationDir = new File(buildDir, 'android')
options.bootClasspath = getAndroidRuntimeJar()
}
}
test { dependsOn androidProjects*.compileAndroid }

@ -0,0 +1,174 @@
/**
*
* 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.smackx.debugger.android;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.*;
import android.util.Log;
import java.io.Reader;
import java.io.Writer;
/**
* Very simple debugger that prints to the android log the sent and received stanzas. Use
* this debugger with caution since printing to the console is an expensive operation that may
* even block the thread since only one thread may print at a time.<p>
* <p/>
* It is possible to not only print the raw sent and received stanzas but also the interpreted
* packets by Smack. By default interpreted packets won't be printed. To enable this feature
* just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>.
*
*/
public class AndroidDebugger implements SmackDebugger {
public static boolean printInterpreted = false;
private XMPPConnection connection = null;
private PacketListener listener = null;
private ConnectionListener connListener = null;
private Writer writer;
private Reader reader;
private ReaderListener readerListener;
private WriterListener writerListener;
public AndroidDebugger(XMPPConnection connection, Writer writer, Reader reader) {
this.connection = connection;
this.writer = writer;
this.reader = reader;
createDebug();
}
/**
* Creates the listeners that will print in the console when new activity is detected.
*/
private void createDebug() {
// Create a special Reader that wraps the main Reader and logs data to the GUI.
ObservableReader debugReader = new ObservableReader(reader);
readerListener = new ReaderListener() {
public void read(String str) {
Log.d("SMACK", "RCV (" + connection.getConnectionCounter() +
"): " + str);
}
};
debugReader.addReaderListener(readerListener);
// Create a special Writer that wraps the main Writer and logs data to the GUI.
ObservableWriter debugWriter = new ObservableWriter(writer);
writerListener = new WriterListener() {
public void write(String str) {
Log.d("SMACK", "SENT (" + connection.getConnectionCounter() +
"): " + str);
}
};
debugWriter.addWriterListener(writerListener);
// Assign the reader/writer objects to use the debug versions. The packet reader
// and writer will use the debug versions when they are created.
reader = debugReader;
writer = debugWriter;
// Create a thread that will listen for all incoming packets and write them to
// the GUI. This is what we call "interpreted" packet data, since it's the packet
// data as Smack sees it and not as it's coming in as raw XML.
listener = new PacketListener() {
public void processPacket(Packet packet) {
if (printInterpreted) {
Log.d("SMACK", "RCV PKT (" + connection.getConnectionCounter() +
"): " + packet.toXML());
}
}
};
connListener = new AbstractConnectionListener() {
public void connectionClosed() {
Log.d("SMACK", "Connection closed (" + connection.getConnectionCounter() + ")");
}
public void connectionClosedOnError(Exception e) {
Log.d("SMACK", "Connection closed due to an exception (" +
connection.getConnectionCounter() + ")");
}
public void reconnectionFailed(Exception e) {
Log.d("SMACK", "Reconnection failed due to an exception (" +
connection.getConnectionCounter() + ")");
}
public void reconnectionSuccessful() {
Log.d("SMACK", "Connection reconnected (" +
connection.getConnectionCounter() + ")");
}
public void reconnectingIn(int seconds) {
Log.d("SMACK", "Connection (" + connection.getConnectionCounter() +
") will reconnect in " + seconds);
}
};
}
public Reader newConnectionReader(Reader newReader) {
((ObservableReader)reader).removeReaderListener(readerListener);
ObservableReader debugReader = new ObservableReader(newReader);
debugReader.addReaderListener(readerListener);
reader = debugReader;
return reader;
}
public Writer newConnectionWriter(Writer newWriter) {
((ObservableWriter)writer).removeWriterListener(writerListener);
ObservableWriter debugWriter = new ObservableWriter(newWriter);
debugWriter.addWriterListener(writerListener);
writer = debugWriter;
return writer;
}
public void userHasLogged(String user) {
String title =
"User logged (" + connection.getConnectionCounter() + "): "
+ user
+ "@"
+ connection.getServiceName()
+ ":"
+ connection.getPort();
Log.d("SMACK", title);
// Add the connection listener to the connection so that the debugger can be notified
// whenever the connection is closed.
connection.addConnectionListener(connListener);
}
public Reader getReader() {
return reader;
}
public Writer getWriter() {
return writer;
}
public PacketListener getReaderListener() {
return listener;
}
public PacketListener getWriterListener() {
return null;
}
}

@ -20,9 +20,9 @@ package org.jivesoftware.smack.bosh;
import java.io.StringReader;
import org.jivesoftware.smack.packet.Packet;
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.sasl.packet.SaslStanzas.Challenge;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jivesoftware.smack.XMPPException.StreamErrorException;
import org.xmlpull.v1.XmlPullParserFactory;
@ -97,8 +97,9 @@ public class BOSHPacketReader implements BOSHClientResponseListener {
BodyQName.create(XMPPBOSHConnection.BOSH_URI, "to"),
connection.getServiceName())
.build());
connection.getSASLAuthentication().authenticated();
connection.processPacket(new Success(parser.nextText()));
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")) {

@ -4,6 +4,7 @@ Smack core components."""
configurations {
compression
dns
sasl
}
dependencies {
@ -34,9 +35,23 @@ task dnsJar(type: Jar) {
include('org/jivesoftware/smack/initializer/**')
}
task saslJar(type: Jar) {
appendix = 'sasl'
dependsOn classes
from sourceSets.main.output
include('org/jivesoftware/smack/SASLAuthentication.class')
include('org/jivesoftware/smack/SmackException.class')
include('org/jivesoftware/smack/XMPPConnection.class')
include('org/jivesoftware/smack/sasl/**')
include('org/jivesoftware/smack/initializer/**')
include('org/jivesoftware/smack/util/StringUtils.class')
include('org/jivesoftware/smack/util/ByteUtils.class')
}
artifacts {
compression compressionJar
dns dnsJar
sasl saslJar
}
class CreateFileTask extends DefaultTask {

@ -33,8 +33,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.sasl.SaslException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.ConnectionException;
@ -60,7 +58,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
private static final String[] DEBUGGERS = new String[] {
"org.jivesoftware.smackx.debugger.EnhancedDebugger",
"org.jivesoftware.smackx.debugger.android.AndroidDebugger",
"org.jivesoftware.smack.debugger.LiteDebugger" };
"org.jivesoftware.smack.debugger.LiteDebugger",
"org.jivesoftware.smack.debugger.ConsoleDebugger" };
/**
* Counter to uniquely identify connections that are created.
@ -347,9 +346,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* @throws XMPPException if an error occurs on the XMPP protocol level.
* @throws SmackException if an error occurs somehwere else besides XMPP protocol level.
* @throws IOException
* @throws SaslException
*/
public void login(String username, String password) throws XMPPException, SmackException, SaslException, IOException {
public void login(String username, String password) throws XMPPException, SmackException, IOException {
login(username, password, "Smack");
}
@ -378,9 +376,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* @throws XMPPException if an error occurs on the XMPP protocol level.
* @throws SmackException if an error occurs somehwere else besides XMPP protocol level.
* @throws IOException
* @throws SaslException
*/
public abstract void login(String username, String password, String resource) throws XMPPException, SmackException, SaslException, IOException;
public abstract void login(String username, String password, String resource) throws XMPPException, SmackException, IOException;
/**
* Logs in to the server anonymously. Very few servers are configured to support anonymous
@ -391,9 +388,8 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* @throws XMPPException if an error occurs on the XMPP protocol level.
* @throws SmackException if an error occurs somehwere else besides XMPP protocol level.
* @throws IOException
* @throws SaslException
*/
public abstract void loginAnonymously() throws XMPPException, SmackException, SaslException, IOException;
public abstract void loginAnonymously() throws XMPPException, SmackException, IOException;
/**
* Notification message saying that the server requires the client to bind a

@ -18,54 +18,37 @@
package org.jivesoftware.smack;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.sasl.SASLAnonymous;
import org.jivesoftware.smack.sasl.SASLCramMD5Mechanism;
import org.jivesoftware.smack.sasl.SASLDigestMD5Mechanism;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLExternalMechanism;
import org.jivesoftware.smack.sasl.SASLGSSAPIMechanism;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure;
import org.jivesoftware.smack.sasl.SASLPlainMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Success;
import javax.security.auth.callback.CallbackHandler;
import javax.security.sasl.SaslException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p>This class is responsible authenticating the user using SASL, binding the resource
* to the connection and establishing a session with the server.</p>
*
* <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to
* register with the server, authenticate using Non-SASL or authenticate using SASL. If the
* server supports SASL then Smack will first try to authenticate using SASL. But if that
* fails then Non-SASL will be tried.</p>
* register with the server or authenticate using SASL. If the
* server supports SASL then Smack will try to authenticate using SASL..</p>
*
* <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
* Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
* {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
* mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
* the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
*
* <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for
* the connection. If no resource is passed in {@link #authenticate(String, String, String)}
* then the server will assign a resource for the connection. In case a resource is passed
* then the server will receive the desired resource but may assign a modified resource for
* the connection.</p>
*
* <p>Once a resource has been binded and if the server supports sessions then Smack will establish
* a session so that instant messaging and presence functionalities may be used.</p>
* {@link #registerSASLMechanism(SASLMechanism)} to register a new mechanisms.
*
* @see org.jivesoftware.smack.sasl.SASLMechanism
*
@ -74,114 +57,91 @@ import java.util.Map;
*/
public class SASLAuthentication {
private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
private static List<String> mechanismsPreferences = new ArrayList<String>();
private static final List<SASLMechanism> REGISTERED_MECHANISMS = new ArrayList<SASLMechanism>();
private XMPPConnection 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 static final Set<String> BLACKLISTED_MECHANISMS = new HashSet<String>();
/**
* The SASL related error condition if there was one provided by the server.
* Registers a new SASL mechanism
*
* @param mechanism a SASLMechanism subclass.
*/
private SASLFailure saslFailure;
static {
// Register SASL mechanisms supported by Smack
registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
supportSASLMechanism("GSSAPI",0);
supportSASLMechanism("DIGEST-MD5",1);
supportSASLMechanism("CRAM-MD5",2);
supportSASLMechanism("PLAIN",3);
supportSASLMechanism("ANONYMOUS",4);
public static void registerSASLMechanism(SASLMechanism mechanism) {
synchronized (REGISTERED_MECHANISMS) {
REGISTERED_MECHANISMS.add(mechanism);
Collections.sort(REGISTERED_MECHANISMS);
}
}
/**
* Registers a new SASL mechanism
* Returns the registered SASLMechanism sorted by the level of preference.
*
* @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
* @param mClass a SASLMechanism subclass.
* @return the registered SASLMechanism sorted by the level of preference.
*/
public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {
implementedMechanisms.put(name, mClass);
public static Map<String, String> getRegisterdSASLMechanisms() {
Map<String, String> answer = new HashMap<String, String>();
synchronized (REGISTERED_MECHANISMS) {
for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
answer.put(mechanism.getClass().getName(), mechanism.getName());
}
}
return answer;
}
/**
* Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
* be possible to authenticate users using the removed SASL mechanism. It also removes the
* mechanism from the supported list.
*
* @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
* Unregister a SASLMechanism by it's full class name. For example
* "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
*
* @param clazz the SASLMechanism class's name
* @return true if the given SASLMechanism was removed, false otherwise
*/
public static void unregisterSASLMechanism(String name) {
implementedMechanisms.remove(name);
mechanismsPreferences.remove(name);
public static boolean unregisterSASLMechanism(String clazz) {
synchronized (REGISTERED_MECHANISMS) {
Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
while (it.hasNext()) {
SASLMechanism mechanism = it.next();
if (mechanism.getClass().getName().equals(clazz)) {
it.remove();
return true;
}
}
}
return false;
}
public static boolean blacklistSASLMechanism(String mechansim) {
synchronized(BLACKLISTED_MECHANISMS) {
return BLACKLISTED_MECHANISMS.add(mechansim);
}
}
/**
* Registers a new SASL mechanism in the specified preference position. The client will try
* to authenticate using the most prefered SASL mechanism that is also supported by the server.
* The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
*
* @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
*/
public static void supportSASLMechanism(String name) {
mechanismsPreferences.add(0, name);
public static boolean unBlacklistSASLMechanism(String mechanism) {
synchronized(BLACKLISTED_MECHANISMS) {
return BLACKLISTED_MECHANISMS.remove(mechanism);
}
}
/**
* Registers a new SASL mechanism in the specified preference position. The client will try
* to authenticate using the most prefered SASL mechanism that is also supported by the server.
* Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.
* A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be
* registered via {@link #registerSASLMechanism(String, Class)}
*
* @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
* @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
*/
public static void supportSASLMechanism(String name, int index) {
mechanismsPreferences.add(index, name);
public static Set<String> getBlacklistedSASLMechanisms() {
synchronized(BLACKLISTED_MECHANISMS) {
return new HashSet<String>(BLACKLISTED_MECHANISMS);
}
}
private final AbstractXMPPConnection connection;
private Collection<String> serverMechanisms = new ArrayList<String>();
private SASLMechanism currentMechanism = null;
/**
* Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
* be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
* is still registered, but will just not be used.
*
* @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
* Boolean indicating if SASL negotiation has finished and was successful.
*/
public static void unsupportSASLMechanism(String name) {
mechanismsPreferences.remove(name);
}
private boolean saslNegotiated;
/**
* Returns the registerd SASLMechanism classes sorted by the level of preference.
*
* @return the registerd SASLMechanism classes sorted by the level of preference.
* Either of type {@link SmackException} or {@link SASLErrorException}
*/
public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
for (String mechanismsPreference : mechanismsPreferences) {
answer.add(implementedMechanisms.get(mechanismsPreference));
}
return answer;
}
private Exception saslException;
SASLAuthentication(XMPPConnection connection) {
super();
SASLAuthentication(AbstractXMPPConnection connection) {
this.connection = connection;
this.init();
}
@ -216,42 +176,16 @@ public class SASLAuthentication {
* @param cbh the CallbackHandler used to get information from the user
* @throws IOException
* @throws XMPPErrorException
* @throws NoResponseException
* @throws SASLErrorException
* @throws ResourceBindingNotOfferedException
* @throws NotConnectedException
* @throws SmackException
*/
public void authenticate(String resource, CallbackHandler cbh) throws IOException,
NoResponseException, XMPPErrorException, SASLErrorException, ResourceBindingNotOfferedException, NotConnectedException {
// Locate the SASLMechanism to use
String selectedMechanism = null;
for (String mechanism : mechanismsPreferences) {
if (implementedMechanisms.containsKey(mechanism)
&& serverMechanisms.contains(mechanism)) {
selectedMechanism = mechanism;
break;
}
}
XMPPErrorException, SASLErrorException, SmackException {
SASLMechanism selectedMechanism = selectMechanism();
if (selectedMechanism != null) {
// A SASL mechanism was found. Authenticate using the selected mechanism and then
// proceed to bind a resource
Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
Constructor<? extends SASLMechanism> constructor;
try {
constructor = mechanismClass.getConstructor(SASLAuthentication.class);
currentMechanism = constructor.newInstance(this);
}
catch (Exception e) {
throw new SaslException("Exception when creating the SASLAuthentication instance",
e);
}
currentMechanism = selectedMechanism;
synchronized (this) {
// Trigger SASL authentication with the selected mechanism. We use
// connection.getHost() since GSAPI requires the FQDN of the server, which
// may not match the XMPP domain.
currentMechanism.authenticate(connection.getHost(), cbh);
currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh);
try {
// Wait until SASL negotiation finishes
wait(connection.getPacketReplyTimeout());
@ -261,18 +195,14 @@ public class SASLAuthentication {
}
}
if (saslFailure != null) {
// SASL authentication failed and the server may have closed the connection
// so throw an exception
throw new SASLErrorException(selectedMechanism, saslFailure);
}
maybeThrowException();
if (!saslNegotiated) {
throw new NoResponseException();
}
}
else {
throw new SaslException(
throw new SmackException(
"SASL Authentication failed. No known authentication mechanisims.");
}
}
@ -291,47 +221,18 @@ public class SASLAuthentication {
* @throws XMPPErrorException
* @throws SASLErrorException
* @throws IOException
* @throws SaslException
* @throws SmackException
*/
public void authenticate(String username, String password, String resource)
throws XMPPErrorException, SASLErrorException, SaslException, IOException,
throws XMPPErrorException, SASLErrorException, IOException,
SmackException {
// Locate the SASLMechanism to use
String selectedMechanism = null;
for (String mechanism : mechanismsPreferences) {
if (implementedMechanisms.containsKey(mechanism)
&& serverMechanisms.contains(mechanism)) {
selectedMechanism = mechanism;
break;
}
}
SASLMechanism selectedMechanism = selectMechanism();
if (selectedMechanism != null) {
// A SASL mechanism was found. Authenticate using the selected mechanism and then
// proceed to bind a resource
Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
try {
Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
currentMechanism = constructor.newInstance(this);
}
catch (Exception e) {
throw new SaslException("Exception when creating the SASLAuthentication instance",
e);
}
currentMechanism = selectedMechanism;
synchronized (this) {
// Trigger SASL authentication with the selected mechanism. We use
// connection.getHost() since GSAPI requires the FQDN of the server, which
// may not match the XMPP domain.
// The serviceName is basically the value that XMPP server sends to the client as
// being
// the location of the XMPP service we are trying to connect to. This should have
// the
// format: host ["/" serv-name ] as per RFC-2831 guidelines
String serviceName = connection.getServiceName();
currentMechanism.authenticate(username, connection.getHost(), serviceName, password);
currentMechanism.authenticate(username, connection.getHost(),
connection.getServiceName(), password);
try {
// Wait until SASL negotiation finishes
wait(connection.getPacketReplyTimeout());
@ -339,21 +240,16 @@ public class SASLAuthentication {
catch (InterruptedException e) {
// Ignore
}
}
if (saslFailure != null) {
// SASL authentication failed and the server may have closed the connection
// so throw an exception
throw new SASLErrorException(selectedMechanism, saslFailure);
}
maybeThrowException();
if (!saslNegotiated) {
throw new NoResponseException();
}
}
else {
throw new SaslException(
throw new SmackException(
"SASL Authentication failed. No known authentication mechanisims.");
}
}
@ -367,14 +263,12 @@ public class SASLAuthentication {
* no username.
*
* @throws SASLErrorException
* @throws IOException
* @throws SaslException
* @throws XMPPErrorException if an error occures while authenticating.
* @throws SmackException if there was no response from the server.
*/
public void authenticateAnonymously() throws SASLErrorException, SaslException, IOException,
public void authenticateAnonymously() throws SASLErrorException,
SmackException, XMPPErrorException {
currentMechanism = new SASLAnonymous(this);
currentMechanism = (new SASLAnonymous()).instanceForAuthentication(connection);
// Wait until SASL negotiation finishes
synchronized (this) {
@ -387,17 +281,24 @@ public class SASLAuthentication {
}
}
if (saslFailure != null) {
// SASL authentication failed and the server may have closed the connection
// so throw an exception
throw new SASLErrorException(currentMechanism.toString(), saslFailure);
}
maybeThrowException();
if (!saslNegotiated) {
throw new NoResponseException();
}
}
private void maybeThrowException() throws SmackException, SASLErrorException {
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);
}
}
}
/**
* 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
@ -411,33 +312,49 @@ public class SASLAuthentication {
}
/**
* Returns true if the user was able to authenticate with the server usins SASL.
*
* @return true if the user was able to authenticate with the server usins SASL.
* Wrapper for {@link #challengeReceived(String, boolean)}, with <code>finalChallenge</code> set
* to <code>false</code>.
*
* @param challenge a base64 encoded string representing the challenge.
* @throws SmackException
*/
public boolean isAuthenticated() {
return saslNegotiated;
public void challengeReceived(String challenge) throws SmackException {
challengeReceived(challenge, false);
}
/**
* The server is challenging the SASL authentication we just sent. Forward the challenge
* to the current SASLMechanism we are using. The SASLMechanism will send a response to
* to the current SASLMechanism we are using. The SASLMechanism will eventually send a response to
* the server. The length of the challenge-response sequence varies according to the
* SASLMechanism in use.
*
* @param challenge a base64 encoded string representing the challenge.
* @throws IOException If a network error occures while authenticating.
* @throws NotConnectedException
* @param finalChallenge true if this is the last challenge send by the server within the success stanza
* @throws SmackException
*/
public void challengeReceived(String challenge) throws IOException, NotConnectedException {
currentMechanism.challengeReceived(challenge);
public void challengeReceived(String challenge, boolean finalChallenge) throws SmackException {
try {
currentMechanism.challengeReceived(challenge, finalChallenge);
} catch (SmackException e) {
authenticationFailed(e);
throw e;
}
}
/**
* Notification message saying that SASL authentication was successful. The next step
* would be to bind the resource.
* @throws SmackException
*/
public void authenticated() {
public void authenticated(Success success) throws SmackException {
// RFC6120 6.3.10 "At the end of the authentication exchange, the SASL server (the XMPP
// "receiving entity") can include "additional data with success" if appropriate for the
// SASL mechanism in use. In XMPP, this is done by including the additional data as the XML
// character data of the <success/> element." The used SASL mechanism should be able to
// verify the data send by the server in the success stanza, if any.
if (success.getData() != null) {
challengeReceived(success.getData(), true);
}
saslNegotiated = true;
// Wake up the thread that is waiting in the #authenticate method
synchronized (this) {
@ -453,18 +370,17 @@ public class SASLAuthentication {
* @see <a href="https://tools.ietf.org/html/rfc6120#section-6.5">RFC6120 6.5</a>
*/
public void authenticationFailed(SASLFailure saslFailure) {
this.saslFailure = saslFailure;
authenticationFailed(new SASLErrorException(currentMechanism.getName(), saslFailure));
}
public void authenticationFailed(Exception exception) {
saslException = exception;
// Wake up the thread that is waiting in the #authenticate method
synchronized (this) {
notify();
}
}
public void send(Packet stanza) throws NotConnectedException {
connection.sendPacket(stanza);
}
/**
* 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
@ -472,6 +388,28 @@ public class SASLAuthentication {
*/
protected void init() {
saslNegotiated = false;
saslFailure = null;
saslException = null;
}
private SASLMechanism selectMechanism() {
// Locate the SASLMechanism to use
SASLMechanism selectedMechanism = null;
Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
// Iterate in SASL Priority order over registered mechanisms
while (it.hasNext()) {
SASLMechanism mechanism = it.next();
String mechanismName = mechanism.getName();
synchronized (BLACKLISTED_MECHANISMS) {
if (BLACKLISTED_MECHANISMS.contains(mechanismName)) {
continue;
}
}
if (serverMechanisms.contains(mechanismName)) {
// Create a new instance of the SASLMechanism for every authentication attempt.
selectedMechanism = mechanism.instanceForAuthentication(connection);
break;
}
}
return selectedMechanism;
}
}

@ -432,7 +432,7 @@ public final class SmackConfiguration {
if (SmackInitializer.class.isAssignableFrom(initClass)) {
SmackInitializer initializer = (SmackInitializer) initClass.newInstance();
List<Exception> exceptions = initializer.initialize();
if (exceptions.size() == 0) {
if (exceptions == null || exceptions.size() == 0) {
LOGGER.log(Level.FINE, "Loaded SmackInitializer " + className);
} else {
for (Exception e : exceptions) {

@ -16,10 +16,7 @@
*/
package org.jivesoftware.smack.sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import java.io.IOException;
import org.jivesoftware.smack.SmackException;
import javax.security.auth.callback.CallbackHandler;
@ -30,32 +27,30 @@ import javax.security.auth.callback.CallbackHandler;
*/
public class SASLAnonymous extends SASLMechanism {
public SASLAnonymous(SASLAuthentication saslAuthentication) {
super(saslAuthentication);
}
protected String getName() {
public String getName() {
return "ANONYMOUS";
}
public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, NotConnectedException {
authenticate();
@Override
public int getPriority() {
return 500;
}
public void authenticate(String username, String host, String password) throws IOException, NotConnectedException {
authenticate();
@Override
protected void authenticateInternal(CallbackHandler cbh)
throws SmackException {
// Nothing to do here
}
protected void authenticate() throws IOException, NotConnectedException {
// Send the authentication to the server
getSASLAuthentication().send(new AuthMechanism(getName(), null));
@Override
protected String getAuthenticationText() throws SmackException {
// TODO Auto-generated method stub
return null;
}
public void challengeReceived(String challenge) throws IOException, NotConnectedException {
// Build the challenge response stanza encoding the response text
// and send the authentication to the server
getSASLAuthentication().send(new Response());
@Override
public SASLAnonymous newInstance() {
return new SASLAnonymous();
}
}

@ -20,7 +20,7 @@ import java.util.HashMap;
import java.util.Map;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.sasl.SASLMechanism.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.SASLFailure;
public class SASLErrorException extends XMPPException {

@ -1,89 +0,0 @@
/**
*
* Copyright the original author or authors
*
* 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 org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.auth.callback.CallbackHandler;
/**
* Implementation of the SASL GSSAPI mechanism
*
* @author Jay Kline
*/
public class SASLGSSAPIMechanism extends SASLMechanism {
public SASLGSSAPIMechanism(SASLAuthentication saslAuthentication) {
super(saslAuthentication);
System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
System.setProperty("java.security.auth.login.config","gss.conf");
}
protected String getName() {
return "GSSAPI";
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server.
* This overrides from the abstract class because the initial token
* needed for GSSAPI is binary, and not safe to put in a string, thus
* getAuthenticationText() cannot be used.
*
* @param username the username of the user being authenticated.
* @param host the hostname where the user account resides.
* @param cbh the CallbackHandler (not used with GSSAPI)
* @throws IOException If a network error occures while authenticating.
* @throws NotConnectedException
*/
public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException {
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String,String>();
props.put(Sasl.SERVER_AUTH,"TRUE");
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
authenticate();
}
/**
* Builds and sends the <tt>auth</tt> stanza to the server.
* This overrides from the abstract class because the initial token
* needed for GSSAPI is binary, and not safe to put in a string, thus
* getAuthenticationText() cannot be used.
*
* @param username the username of the user being authenticated.
* @param host the hostname where the user account resides.
* @param password the password of the user (ignored for GSSAPI)
* @throws IOException If a network error occures while authenticating.
* @throws NotConnectedException
*/
public void authenticate(String username, String host, String password) throws IOException, SaslException, NotConnectedException {
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String, String>();
props.put(Sasl.SERVER_AUTH,"TRUE");
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this);
authenticate();
}
}

@ -1,6 +1,6 @@
/**
*
* Copyright 2003-2007 Jive Software.
* Copyright 2003-2007 Jive Software, 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.
@ -16,25 +16,14 @@
*/
package org.jivesoftware.smack.sasl;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.AuthMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStanzas.Response;
import org.jivesoftware.smack.util.StringUtils;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.sasl.RealmCallback;
import javax.security.sasl.RealmChoiceCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
/**
* Base class for SASL mechanisms. Subclasses must implement these methods:
@ -44,9 +33,9 @@ import javax.security.sasl.SaslException;
* Subclasses will likely want to implement their own versions of these methods:
* <li>{@link #authenticate(String, String, String, String)} -- Initiate authentication stanza using the
* deprecated method.</li>
* <li>{@link #authenticate(String, CallbackHandler)} -- Initiate authentication stanza
* <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>
* <li>{@link #challengeReceived(String, boolean)} -- Handle a challenge from the server.</li>
* </ul>
*
* Basic XMPP SASL authentication steps:
@ -73,26 +62,40 @@ import javax.security.sasl.SaslException;
*
* @author Jay Kline
*/
public abstract class SASLMechanism implements CallbackHandler {
public abstract class SASLMechanism implements Comparable<SASLMechanism> {
public static final String CRAMMD5 = "CRAM-MD5";
public static final String DIGESTMD5 = "DIGEST-MD5";
public static final String EXTERNAL = "EXTERNAL";
public static final String GSSAPI = "GSSAPI";
public static final String PLAIN = "PLAIN";
protected XMPPConnection connection;
private SASLAuthentication saslAuthentication;
protected SaslClient sc;
/**
* authcid
*/
protected String authenticationId;
protected String password;
protected String hostname;
public SASLMechanism(SASLAuthentication saslAuthentication) {
this.saslAuthentication = saslAuthentication;
}
/**
* The name of the XMPP service
*/
protected String serviceName;
/**
* The users password
*/
protected String password;
protected String host;
/**
* 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, CallbackHandler)} whenever possible.
* authentication is not recommended, since it is very inflexible. Use
* {@link #authenticate(String, String, CallbackHandler)} whenever possible.
*
* Explanation of auth stanza:
*
* The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName
* The client authentication stanza needs to include the digest-uri of the form: xmpp/serviceName
* From RFC-2831:
* digest-uri = "digest-uri" "=" digest-uri-value
* digest-uri-value = serv-type "/" host [ "/" serv-name ]
@ -129,70 +132,67 @@ public abstract class SASLMechanism implements CallbackHandler {
* @param serviceName the xmpp service location - used by the SASL client in digest-uri creation
* serviceName format is: host [ "/" serv-name ] as per RFC-2831
* @param password the password for this account.
* @throws IOException If a network error occurs while authenticating.
* @throws SaslException
* @throws SmackException If a network error occurs while authenticating.
* @throws NotConnectedException
*/
public void authenticate(String username, String host, String serviceName, String password) throws IOException, SaslException, NotConnectedException {
//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.
public final void authenticate(String username, String host, String serviceName, String password)
throws SmackException, NotConnectedException {
this.authenticationId = username;
this.host = host;
this.serviceName = serviceName;
this.password = password;
this.hostname = host;
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String,String>();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", serviceName, props, this);
authenticateInternal();
authenticate();
}
protected void authenticateInternal() throws SmackException {
}
/**
* 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 host the hostname where the user account resides.
* @param serviceName the xmpp service location
* @param cbh the CallbackHandler to obtain user information.
* @throws IOException If a network error occures while authenticating.
* @throws SaslException If a protocol error occurs or the user is not authenticated.
* @throws SmackException
* @throws NotConnectedException
*/
public void authenticate(String host, CallbackHandler cbh) throws IOException, SaslException, NotConnectedException {
String[] mechanisms = { getName() };
Map<String,String> props = new HashMap<String,String>();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
public void authenticate(String host,String serviceName, CallbackHandler cbh)
throws SmackException, NotConnectedException {
this.host = host;
this.serviceName = serviceName;
authenticateInternal(cbh);
authenticate();
}
protected void authenticate() throws IOException, SaslException, NotConnectedException {
String authenticationText = null;
if (sc.hasInitialResponse()) {
byte[] response = sc.<