/*******************************************************************************
* Copyright (c) 2007, Dave Whitla
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package au.net.ocean.httpclient.auth.spnego;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.auth.AuthPolicy;
import org.apache.commons.httpclient.auth.AuthScope;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import java.io.IOException;
import java.security.PrivilegedActionException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Created by dwhitla at Apr 15, 2007 3:09:49 PM
* <p/>
* Decorator
*
* @author <a href="mailto:dave.whitla@ocean.net.au">Dave Whitla</a>
* @version $Id: SPNegoHTTPClient.java 0 Apr 15, 2007 3:09:49 PM dwhitla $
*/
public class SPNegoHTTPClient extends HttpClient {
private static final Logger LOGGER = Logger.getLogger(SPNegoHTTPClient.class.getName());
private static final boolean MESSAGE_CONFIDENTIALITY = true;
private static final boolean MESSAGE_INTEGRITY = true;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Negotiate";
private SPNegoCredentials credentials;
private boolean mutualAuth;
public SPNegoHTTPClient(SPNegoCredentials credentials, boolean mutualAuth) {
super();
this.credentials = credentials;
this.mutualAuth = mutualAuth;
AuthPolicy.registerAuthScheme(SPNegoAuthScheme.IDENTIFIER, SPNegoAuthScheme.class);
List<String> authPriority = Arrays.asList(SPNegoAuthScheme.IDENTIFIER);
Object existingAuthPriority = getParams().getParameter(AuthPolicy.AUTH_SCHEME_PRIORITY);
if (existingAuthPriority != null) {
authPriority.addAll((List<String>) existingAuthPriority);
}
getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPriority);
}
/**
* Executes the given {@link org.apache.commons.httpclient.HttpMethod HTTP method}.
*
* @param method the {@link org.apache.commons.httpclient.HttpMethod HTTP method} to execute.
* @return the method's response code
* @throws java.io.IOException If an I/O (transport) error occurs. Some transport exceptions
* can be recovered from.
* @throws org.apache.commons.httpclient.HttpException
* If a protocol exception occurs. Usually protocol exceptions
* cannot be recovered from.
*/
@Override
public int executeMethod(HttpMethod method) throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, null);
}
try {
return super.executeMethod(method);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
/**
* Executes the given {@link org.apache.commons.httpclient.HttpMethod HTTP method} using custom
* {@link org.apache.commons.httpclient.HostConfiguration host configuration}.
*
* @param hostConfiguration The {@link org.apache.commons.httpclient.HostConfiguration host configuration} to use.
* @param method the {@link org.apache.commons.httpclient.HttpMethod HTTP method} to execute.
* @return the method's response code
* @throws java.io.IOException If an I/O (transport) error occurs. Some transport exceptions
* can be recovered from.
* @throws org.apache.commons.httpclient.HttpException
* If a protocol exception occurs. Usually protocol exceptions
* cannot be recovered from.
* @since 2.0
*/
@Override
public int executeMethod(final HostConfiguration hostConfiguration, final HttpMethod method) throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, null);
}
try {
return super.executeMethod(hostConfiguration, method);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
/**
* Executes the given {@link org.apache.commons.httpclient.HttpMethod HTTP method} using the given custom
* {@link org.apache.commons.httpclient.HostConfiguration host configuration} with the given custom
* {@link org.apache.commons.httpclient.HttpState HTTP state}.
*
* @param hostconfig The {@link org.apache.commons.httpclient.HostConfiguration host configuration} to use.
* @param method the {@link org.apache.commons.httpclient.HttpMethod HTTP method} to execute.
* @param state the {@link org.apache.commons.httpclient.HttpState HTTP state} to use when executing the method.
* If <code>null</code>, the state returned by {@link #getState} will be used instead.
* @return the method's response code
* @throws java.io.IOException If an I/O (transport) error occurs. Some transport exceptions
* can be recovered from.
* @throws org.apache.commons.httpclient.HttpException
* If a protocol exception occurs. Usually protocol exceptions
* cannot be recovered from.
* @since 2.0
*/
@Override
public int executeMethod(HostConfiguration hostconfig, final HttpMethod method, final HttpState state) throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, null);
}
try {
return super.executeMethod(hostconfig, method, state);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
public int executeMethod(HttpMethod method, String servicePrincipal) throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, servicePrincipal);
}
try {
return super.executeMethod(method);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
public int executeMethod(final HostConfiguration hostConfiguration, final HttpMethod method, String servicePrincipal)
throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, servicePrincipal);
}
try {
return super.executeMethod(hostConfiguration, method);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
public int executeMethod(HostConfiguration hostconfig,
final HttpMethod method,
final HttpState state,
String servicePrincipal) throws IOException {
if (method.getDoAuthentication()) {
setupGSSContext(method, servicePrincipal);
}
try {
return super.executeMethod(hostconfig, method, state);
} finally {
if (method.getDoAuthentication()) {
closeGSSContext();
}
}
}
private void setupGSSContext(HttpMethod method, String servicePrincipal) throws HttpException {
URI uri = method.getURI();
if (servicePrincipal == null || servicePrincipal.trim().length() == 0) {
servicePrincipal = "HTTP/" + uri.getHost();
}
try {
credentials.createGSSContext(servicePrincipal);
} catch (PrivilegedActionException e) {
throw new HttpException("PrivilegedActionException", e);
} catch (GSSException e) {
throw new HttpException("GSSException", e);
}
GSSContext gssContext = credentials.getGSSContext();
try {
// hack because HttpClient preemtive auth is broken wrt spnego as at 3.0.1
if (getParams().isAuthenticationPreemptive()) {
LOGGER.log(Level.INFO, "Using preemptive SPNego authentication");
byte[] token = new Base64().encode(gssContext.initSecContext(new byte[0], 0, 0));
LOGGER.log(Level.INFO, "Sending \"{0}\" {1} header", new String[]{TOKEN_PREFIX, AUTHORIZATION_HEADER});
method.setRequestHeader(AUTHORIZATION_HEADER, MessageFormat.format("{0} {1}", TOKEN_PREFIX, new String(token)));
getParams().setAuthenticationPreemptive(false);
}
gssContext.requestMutualAuth(mutualAuth);
gssContext.requestConf(MESSAGE_CONFIDENTIALITY);
gssContext.requestInteg(MESSAGE_INTEGRITY);
} catch (GSSException e) {
LOGGER.log(Level.SEVERE, "Caught GSSException setting options on GSSContext", e);
}
AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort(), credentials.getRealm());
getState().setCredentials(authScope, credentials);
}
private void closeGSSContext() {
try {
credentials.closeGSSContext();
} catch (GSSException e) {
LOGGER.log(Level.WARNING, "Caught GSSException closing context", e);
}
}
}