/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.engine.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.AuthenticationInfo;
import org.restlet.data.ChallengeRequest;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Header;
import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.engine.Engine;
import org.restlet.engine.header.ChallengeRequestReader;
import org.restlet.engine.header.ChallengeWriter;
import org.restlet.engine.header.HeaderConstants;
import org.restlet.engine.header.HeaderReader;
import org.restlet.util.Series;
/**
* Authentication utilities.
*
* @author Jerome Louvel
* @author Ray Waldin (ray@waldin.net)
*/
public class AuthenticatorUtils {
/**
* Indicates if any of the objects is null.
*
* @param objects
* The objects to test.
* @return True if any of the objects is null.
*/
public static boolean anyNull(Object... objects) {
for (final Object o : objects) {
if (o == null) {
return true;
}
}
return false;
}
/**
* Formats an authentication information as a HTTP header value. The header
* is {@link HeaderConstants#HEADER_AUTHENTICATION_INFO}.
*
* @param info
* The authentication information to format.
* @return The {@link HeaderConstants#HEADER_AUTHENTICATION_INFO} header
* value.
*/
public static String formatAuthenticationInfo(AuthenticationInfo info) {
ChallengeWriter cw = new ChallengeWriter();
boolean firstParameter = true;
if (info != null) {
if (info.getNextServerNonce() != null
&& info.getNextServerNonce().length() > 0) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("nextnonce",
info.getNextServerNonce());
firstParameter = false;
}
if (info.getQuality() != null && info.getQuality().length() > 0) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("qop", info.getQuality());
firstParameter = false;
if (info.getNonceCount() > 0) {
cw.appendChallengeParameter("nc",
formatNonceCount(info.getNonceCount()));
}
}
if (info.getResponseDigest() != null
&& info.getResponseDigest().length() > 0) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendQuotedChallengeParameter("rspauth",
info.getResponseDigest());
firstParameter = false;
}
if (info.getClientNonce() != null
&& info.getClientNonce().length() > 0) {
cw.setFirstChallengeParameter(firstParameter);
cw.appendChallengeParameter("cnonce", info.getClientNonce());
firstParameter = false;
}
}
return cw.toString();
}
/**
* Formats a given nonce count as a HTTP header value. The header is
* {@link HeaderConstants#HEADER_AUTHENTICATION_INFO}.
*
* @param nonceCount
* The given nonce count.
* @return The formatted value of the given nonce count.
*/
public static String formatNonceCount(int nonceCount) {
StringBuilder result = new StringBuilder(
Integer.toHexString(nonceCount));
while (result.length() < 8) {
result.insert(0, '0');
}
return result.toString();
}
/**
* Formats a challenge request as a HTTP header value. The header is
* {@link HeaderConstants#HEADER_WWW_AUTHENTICATE}. The default
* implementation relies on
* {@link AuthenticatorHelper#formatRequest(ChallengeWriter, ChallengeRequest, Response, Series)}
* to append all parameters from {@link ChallengeRequest#getParameters()}.
*
* @param challenge
* The challenge request to format.
* @param response
* The parent response.
* @param httpHeaders
* The current response HTTP headers.
* @return The {@link HeaderConstants#HEADER_WWW_AUTHENTICATE} header value.
*/
public static String formatRequest(ChallengeRequest challenge,
Response response, Series<Header> httpHeaders) {
String result = null;
if (challenge == null) {
Context.getCurrentLogger().warning(
"No challenge response to format.");
} else if (challenge.getScheme() == null) {
Context.getCurrentLogger().warning(
"A challenge response must have a scheme defined.");
} else if (challenge.getScheme().getTechnicalName() == null) {
Context.getCurrentLogger().warning(
"A challenge scheme must have a technical name defined.");
} else {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();
if (challenge.getRawValue() != null) {
cw.append(challenge.getRawValue());
} else {
AuthenticatorHelper helper = Engine.getInstance().findHelper(
challenge.getScheme(), false, true);
if (helper != null) {
try {
helper.formatRequest(cw, challenge, response,
httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(
Level.WARNING,
"Unable to format the challenge request: "
+ challenge, e);
}
} else {
result = "?";
Context.getCurrentLogger().warning(
"Challenge scheme " + challenge.getScheme()
+ " not supported by the Restlet engine.");
}
}
result = (cw.getBuffer().length() > cwInitialLength) ? cw
.toString() : null;
}
return result;
}
/**
* Formats a challenge response as a HTTP header value. The header is
* {@link HeaderConstants#HEADER_AUTHORIZATION}. The default implementation
* relies on
* {@link AuthenticatorHelper#formatResponse(ChallengeWriter, ChallengeResponse, Request, Series)}
* unless some custom credentials are provided via
*
* @param challenge
* The challenge response to format.
* @param request
* The parent request.
* @param httpHeaders
* The current request HTTP headers.
* @return The {@link HeaderConstants#HEADER_AUTHORIZATION} header value.
* @throws IOException
* @link ChallengeResponse#getCredentials()}.
*/
public static String formatResponse(ChallengeResponse challenge,
Request request, Series<Header> httpHeaders) {
String result = null;
if (challenge == null) {
Context.getCurrentLogger().warning(
"No challenge response to format.");
} else if (challenge.getScheme() == null) {
Context.getCurrentLogger().warning(
"A challenge response must have a scheme defined.");
} else if (challenge.getScheme().getTechnicalName() == null) {
Context.getCurrentLogger().warning(
"A challenge scheme must have a technical name defined.");
} else {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();
if (challenge.getRawValue() != null) {
cw.append(challenge.getRawValue());
} else {
AuthenticatorHelper helper = Engine.getInstance().findHelper(
challenge.getScheme(), true, false);
if (helper != null) {
try {
helper.formatResponse(cw, challenge, request,
httpHeaders);
} catch (Exception e) {
Context.getCurrentLogger().log(
Level.WARNING,
"Unable to format the challenge response: "
+ challenge, e);
}
} else {
Context.getCurrentLogger().warning(
"Challenge scheme " + challenge.getScheme()
+ " not supported by the Restlet engine.");
}
}
result = (cw.getBuffer().length() > cwInitialLength) ? cw
.toString() : null;
}
return result;
}
/**
* Parses the "Authentication-Info" header.
*
* @param header
* The header value to parse.
* @return The equivalent {@link AuthenticationInfo} instance.
* @throws IOException
*/
public static AuthenticationInfo parseAuthenticationInfo(String header) {
AuthenticationInfo result = null;
HeaderReader<Parameter> hr = new HeaderReader<Parameter>(header);
try {
String nextNonce = null;
String qop = null;
String responseAuth = null;
String cnonce = null;
int nonceCount = 0;
Parameter param = hr.readParameter();
while (param != null) {
try {
if ("nextnonce".equals(param.getName())) {
nextNonce = param.getValue();
} else if ("qop".equals(param.getName())) {
qop = param.getValue();
} else if ("rspauth".equals(param.getName())) {
responseAuth = param.getValue();
} else if ("cnonce".equals(param.getName())) {
cnonce = param.getValue();
} else if ("nc".equals(param.getName())) {
nonceCount = Integer.parseInt(param.getValue(), 16);
}
if (hr.skipValueSeparator()) {
param = hr.readParameter();
} else {
param = null;
}
} catch (Exception e) {
Context.getCurrentLogger()
.log(Level.WARNING,
"Unable to parse the authentication info header parameter",
e);
}
}
result = new AuthenticationInfo(nextNonce, nonceCount, cnonce, qop,
responseAuth);
} catch (IOException e) {
Context.getCurrentLogger()
.log(Level.WARNING,
"Unable to parse the authentication info header: "
+ header, e);
}
return result;
}
/**
* Parses an authenticate header into a list of challenge request. The
* header is {@link HeaderConstants#HEADER_WWW_AUTHENTICATE}.
*
* @param header
* The HTTP header value to parse.
* @param httpHeaders
* The current response HTTP headers.
* @return The list of parsed challenge request.
*/
public static List<ChallengeRequest> parseRequest(Response response,
String header, Series<Header> httpHeaders) {
List<ChallengeRequest> result = new ArrayList<ChallengeRequest>();
if (header != null) {
result = new ChallengeRequestReader(header).readValues();
for (ChallengeRequest cr : result) {
// Give a chance to the authenticator helper to do further
// parsing
AuthenticatorHelper helper = Engine.getInstance().findHelper(
cr.getScheme(), true, false);
if (helper != null) {
helper.parseRequest(cr, response, httpHeaders);
} else {
Context.getCurrentLogger().warning(
"Couldn't find any helper support the "
+ cr.getScheme() + " challenge scheme.");
}
}
}
return result;
}
/**
* Parses an authorization header into a challenge response. The header is
* {@link HeaderConstants#HEADER_AUTHORIZATION}.
*
* @param request
* The parent request.
* @param header
* The authorization header.
* @param httpHeaders
* The current request HTTP headers.
* @return The parsed challenge response.
*/
public static ChallengeResponse parseResponse(Request request,
String header, Series<Header> httpHeaders) {
ChallengeResponse result = null;
if (header != null) {
int space = header.indexOf(' ');
if (space != -1) {
String scheme = header.substring(0, space);
String rawValue = header.substring(space + 1);
result = new ChallengeResponse(new ChallengeScheme("HTTP_"
+ scheme, scheme));
result.setRawValue(rawValue);
}
}
if (result != null) {
// Give a chance to the authenticator helper to do further parsing
AuthenticatorHelper helper = Engine.getInstance().findHelper(
result.getScheme(), true, false);
if (helper != null) {
helper.parseResponse(result, request, httpHeaders);
} else {
Context.getCurrentLogger().warning(
"Couldn't find any helper support the "
+ result.getScheme() + " challenge scheme.");
}
}
return result;
}
/**
* Updates a {@link ChallengeResponse} object according to given request and
* response.
*
* @param challengeResponse
* The challengeResponse to update.
* @param request
* The request.
* @param response
* The response.
*/
public static void update(ChallengeResponse challengeResponse,
Request request, Response response) {
ChallengeRequest challengeRequest = null;
for (ChallengeRequest c : response.getChallengeRequests()) {
if (challengeResponse.getScheme().equals(c.getScheme())) {
challengeRequest = c;
break;
}
}
String realm = null;
String nonce = null;
if (challengeRequest != null) {
realm = challengeRequest.getRealm();
nonce = challengeRequest.getServerNonce();
challengeResponse.setOpaque(challengeRequest.getOpaque());
}
challengeResponse.setRealm(realm);
challengeResponse.setServerNonce(nonce);
challengeResponse.setDigestRef(new Reference(request.getResourceRef()
.getPath()));
}
/**
* Optionally updates the request with a challenge response before sending
* it. This is sometimes useful for authentication schemes that aren't based
* on the Authorization header but instead on URI query parameters or other
* headers. By default it returns the resource URI reference unchanged.
*
* @param resourceRef
* The resource URI reference to update.
* @param challengeResponse
* The challenge response provided.
* @param request
* The request to update.
* @return The original URI reference if unchanged or a new one if updated.
*/
public static Reference updateReference(Reference resourceRef,
ChallengeResponse challengeResponse, Request request) {
if (challengeResponse != null) {
AuthenticatorHelper helper = Engine.getInstance().findHelper(
challengeResponse.getScheme(), true, false);
if (helper != null) {
resourceRef = helper.updateReference(resourceRef,
challengeResponse, request);
} else {
Context.getCurrentLogger().warning(
"Challenge scheme " + challengeResponse.getScheme()
+ " not supported by the Restlet engine.");
}
}
return resourceRef;
}
/**
* Private constructor to ensure that the class acts as a true utility class
* i.e. it isn't instantiable and extensible.
*/
private AuthenticatorUtils() {
}
}