// Construct the initial bind request to send to the server. In this case,
// we'll simply indicate that we want to use DIGEST-MD5 so the server will
// send us the challenge.
BindRequestProtocolOp bindRequest1 =
new BindRequestProtocolOp(bindDN.toByteString(),
SASL_MECHANISM_DIGEST_MD5, null);
// FIXME -- Should we include request controls in both stages or just the
// second stage?
LDAPMessage requestMessage1 =
new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
try
{
writer.writeMessage(requestMessage1);
}
catch (IOException ioe)
{
Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
throw new ClientException(
LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
}
catch (Exception e)
{
Message message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
throw new ClientException(LDAPResultCode.CLIENT_SIDE_ENCODING_ERROR,
message, e);
}
// Read the response from the server.
LDAPMessage responseMessage1;
try
{
responseMessage1 = reader.readMessage();
if (responseMessage1 == null)
{
Message message =
ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
throw new ClientException(LDAPResultCode.CLIENT_SIDE_SERVER_DOWN,
message);
}
}
catch (IOException ioe)
{
Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
throw new ClientException(
LDAPResultCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
}
catch (ASN1Exception ae)
{
Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ae));
throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
message, ae);
}
catch (LDAPException le)
{
Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(le));
throw new ClientException(LDAPResultCode.CLIENT_SIDE_DECODING_ERROR,
message, le);
}
catch (Exception e)
{
Message message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
throw new ClientException(
LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
}
// Look at the protocol op from the response. If it's a bind response, then
// continue. If it's an extended response, then it could be a notice of
// disconnection so check for that. Otherwise, generate an error.
switch (responseMessage1.getProtocolOpType())
{
case OP_TYPE_BIND_RESPONSE:
// We'll deal with this later.
break;
case OP_TYPE_EXTENDED_RESPONSE:
ExtendedResponseProtocolOp extendedResponse =
responseMessage1.getExtendedResponseProtocolOp();
String responseOID = extendedResponse.getOID();
if ((responseOID != null) &&
responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
{
Message message = ERR_LDAPAUTH_SERVER_DISCONNECT.
get(extendedResponse.getResultCode(),
extendedResponse.getErrorMessage());
throw new LDAPException(extendedResponse.getResultCode(), message);
}
else
{
Message message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(
String.valueOf(extendedResponse));
throw new ClientException(LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR,
message);
}
default:
Message message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(
String.valueOf(responseMessage1.getProtocolOp()));
throw new ClientException(
LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message);
}
// Make sure that the bind response has the "SASL bind in progress" result
// code.
BindResponseProtocolOp bindResponse1 =
responseMessage1.getBindResponseProtocolOp();
int resultCode1 = bindResponse1.getResultCode();
if (resultCode1 != LDAPResultCode.SASL_BIND_IN_PROGRESS)
{
Message errorMessage = bindResponse1.getErrorMessage();
if (errorMessage == null)
{
errorMessage = Message.EMPTY;
}
Message message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
get(SASL_MECHANISM_DIGEST_MD5, resultCode1,
LDAPResultCode.toString(resultCode1), errorMessage);
throw new LDAPException(resultCode1, errorMessage, message,
bindResponse1.getMatchedDN(), null);
}
// Make sure that the bind response contains SASL credentials with the
// information to use for the next stage of the bind.
ByteString serverCredentials =
bindResponse1.getServerSASLCredentials();
if (serverCredentials == null)
{
Message message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get();
throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
}
// Parse the server SASL credentials to get the necessary information. In
// particular, look at the realm, the nonce, the QoP modes, and the charset.
// We'll only care about the realm if none was provided in the SASL
// properties and only one was provided in the server SASL credentials.
String credString = serverCredentials.toString();
String lowerCreds = toLowerCase(credString);
String nonce = null;
boolean useUTF8 = false;
int pos = 0;
int length = credString.length();
while (pos < length)
{
int equalPos = credString.indexOf('=', pos+1);
if (equalPos < 0)
{
// This is bad because we're not at the end of the string but we don't
// have a name/value delimiter.
Message message =
ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
credString, pos);
throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
}
String tokenName = lowerCreds.substring(pos, equalPos);
StringBuilder valueBuffer = new StringBuilder();
pos = readToken(credString, equalPos+1, length, valueBuffer);
String tokenValue = valueBuffer.toString();
if (tokenName.equals("charset"))
{
// The value must be the string "utf-8". If not, that's an error.
if (! tokenValue.equalsIgnoreCase("utf-8"))
{
Message message =
ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue);
throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
}
useUTF8 = true;
}
else if (tokenName.equals("realm"))
{
// This will only be of interest to us if there is only a single realm
// in the server credentials and none was provided as a client-side
// property.
if (! realmSetFromProperty)
{
if (realm == null)
{
// No other realm was specified, so we'll use this one for now.
realm = tokenValue;
}
else
{
// This must mean that there are multiple realms in the server
// credentials. In that case, we'll not provide any realm at all.
// To make sure that happens, pretend that the client specified the
// realm.
realm = null;
realmSetFromProperty = true;
}
}
}
else if (tokenName.equals("nonce"))
{
nonce = tokenValue;
}
else if (tokenName.equals("qop"))
{
// The QoP modes provided by the server should be a comma-delimited
// list. Decode that list and make sure the QoP we have chosen is in
// that list.
StringTokenizer tokenizer = new StringTokenizer(tokenValue, ",");
LinkedList<String> qopModes = new LinkedList<String>();
while (tokenizer.hasMoreTokens())
{
qopModes.add(toLowerCase(tokenizer.nextToken().trim()));
}
if (! qopModes.contains(qop))
{
Message message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER.
get(qop, tokenValue);
throw new ClientException(LDAPResultCode.CLIENT_SIDE_PARAM_ERROR,
message);
}
}
else
{
// Other values may have been provided, but they aren't of interest to
// us because they shouldn't change anything about the way we encode the
// second part of the request. Rather than attempt to examine them,
// we'll assume that the server sent a valid response.
}
}
// Make sure that the nonce was included in the response from the server.
if (nonce == null)
{
Message message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get();
throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, message);
}
// Generate the cnonce that we will use for this request.
String cnonce = generateCNonce();
// Generate the response digest, and initialize the necessary remaining
// variables to use in the generation of that digest.
String nonceCount = "00000001";
String charset = (useUTF8 ? "UTF-8" : "ISO-8859-1");
String responseDigest;
try
{
responseDigest = generateDigestMD5Response(authID, authzID,
bindPassword, realm,
nonce, cnonce, nonceCount,
digestURI, qop, charset);
}
catch (ClientException ce)
{
throw ce;
}
catch (Exception e)
{
Message message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST.
get(getExceptionMessage(e));
throw new ClientException(
LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
}
// Generate the SASL credentials for the second bind request.
StringBuilder credBuffer = new StringBuilder();
credBuffer.append("username=\"");
credBuffer.append(authID);
credBuffer.append("\"");
if (realm != null)
{
credBuffer.append(",realm=\"");
credBuffer.append(realm);
credBuffer.append("\"");
}
credBuffer.append(",nonce=\"");
credBuffer.append(nonce);
credBuffer.append("\",cnonce=\"");
credBuffer.append(cnonce);
credBuffer.append("\",nc=");
credBuffer.append(nonceCount);
credBuffer.append(",qop=");
credBuffer.append(qop);
credBuffer.append(",digest-uri=\"");
credBuffer.append(digestURI);
credBuffer.append("\",response=");
credBuffer.append(responseDigest);
if (useUTF8)
{
credBuffer.append(",charset=utf-8");
}
if (authzID != null)
{
credBuffer.append(",authzid=\"");
credBuffer.append(authzID);
credBuffer.append("\"");
}
// Generate and send the second bind request.
BindRequestProtocolOp bindRequest2 =
new BindRequestProtocolOp(bindDN.toByteString(),
SASL_MECHANISM_DIGEST_MD5,
ByteString.valueOf(credBuffer.toString()));
LDAPMessage requestMessage2 =
new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
requestControls);