/*
* Copyright (c) 2002-2010 Gargoyle Software Inc.
*
* 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 com.gargoylesoftware.htmlunit;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
import org.apache.commons.httpclient.auth.CredentialsProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Default HtmlUnit implementation of the <tt>CredentialsProvider</tt> interface. Provides
* credentials for both web servers and proxies. Supports NTLM authentication, Digest
* authentication, and Basic HTTP authentication.
*
* @version $Revision: 5301 $
* @author Daniel Gredler
* @author Vikram Shitole
* @author Marc Guillemot
* @author Ahmed Ashour
*/
public class DefaultCredentialsProvider implements CredentialsProvider, Serializable {
private static final long serialVersionUID = 1036331144926557053L;
private static final Log LOG = LogFactory.getLog(DefaultCredentialsProvider.class);
private final Map<AuthScopeProxy, Credentials> credentials_ = new HashMap<AuthScopeProxy, Credentials>();
private final Map<AuthScopeProxy, Credentials> proxyCredentials_ = new HashMap<AuthScopeProxy, Credentials>();
private final Set<Object> answerMarks_ = Collections.synchronizedSortedSet(new TreeSet<Object>());
/**
* Creates a new <tt>DefaultCredentialsProvider</tt> instance.
*/
public DefaultCredentialsProvider() {
// nothing
}
/**
* Adds credentials for the specified username/password for any host/port/realm combination.
* The credentials may be for any authentication scheme, including NTLM, digest and basic
* HTTP authentication. If you are using sensitive username/password information, please do
* NOT use this method. If you add credentials using this method, any server that requires
* authentication may receive the specified username and password.
* @param username the username for the new credentials
* @param password the password for the new credentials
*/
public void addCredentials(final String username, final String password) {
addCredentials(username, password, AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);
}
/**
* Adds credentials for the specified username/password on the specified host/port for the
* specified realm. The credentials may be for any authentication scheme, including NTLM,
* digest and basic HTTP authentication.
* @param username the username for the new credentials
* @param password the password for the new credentials
* @param host the host to which to the new credentials apply (<tt>null</tt> if applicable to any host)
* @param port the port to which to the new credentials apply (negative if applicable to any port)
* @param realm the realm to which to the new credentials apply (<tt>null</tt> if applicable to any realm)
*/
public void addCredentials(final String username, final String password, final String host,
final int port, final String realm) {
final AuthScopeProxy scope = new AuthScopeProxy(host, port, realm, AuthScope.ANY_SCHEME);
final Credentials c = new UsernamePasswordCredentialsExt(username, password);
credentials_.put(scope, c);
clearAnswered(); // don't need to be precise, will cause in worst case one extra request
}
/**
* Adds proxy credentials for the specified username/password for any host/port/realm combination.
* @param username the username for the new credentials
* @param password the password for the new credentials
*/
public void addProxyCredentials(final String username, final String password) {
addProxyCredentials(username, password, AuthScope.ANY_HOST, AuthScope.ANY_PORT);
}
/**
* Adds proxy credentials for the specified username/password on the specified host/port.
* @param username the username for the new credentials
* @param password the password for the new credentials
* @param host the host to which to the new credentials apply (<tt>null</tt> if applicable to any host)
* @param port the port to which to the new credentials apply (negative if applicable to any port)
*/
public void addProxyCredentials(final String username, final String password, final String host, final int port) {
final AuthScopeProxy scope = new AuthScopeProxy(host, port, AuthScope.ANY_REALM, AuthScope.ANY_SCHEME);
final Credentials c = new UsernamePasswordCredentialsExt(username, password);
proxyCredentials_.put(scope, c);
clearAnswered(); // don't need to be precise, will cause in worst case one extra request
}
/**
* Adds NTLM credentials for the specified username/password on the specified host/port.
* @param username the username for the new credentials; should not include the domain to authenticate with;
* for example: <tt>"user"</tt> is correct whereas <tt>"DOMAIN\\user"</tt> is not
* @param password the password for the new credentials
* @param host the host to which to the new credentials apply (<tt>null</tt> if applicable to any host)
* @param port the port to which to the new credentials apply (negative if applicable to any port)
* @param clientHost the host the authentication request is originating from; essentially, the computer name for
* this machine.
* @param clientDomain the domain to authenticate within
*/
public void addNTLMCredentials(final String username, final String password, final String host,
final int port, final String clientHost, final String clientDomain) {
final AuthScopeProxy scope = new AuthScopeProxy(host, port, AuthScope.ANY_REALM, AuthScope.ANY_SCHEME);
final Credentials c = new NTCredentialsExt(username, password, clientHost, clientDomain);
credentials_.put(scope, c);
clearAnswered(); // don't need to be precise, will cause in worst case one extra request
}
/**
* Adds NTLM proxy credentials for the specified username/password on the specified host/port.
* @param username the username for the new credentials; should not include the domain to authenticate with;
* for example: <tt>"user"</tt> is correct whereas <tt>"DOMAIN\\user"</tt> is not.
* @param password the password for the new credentials
* @param host the host to which to the new credentials apply (<tt>null</tt> if applicable to any host)
* @param port the port to which to the new credentials apply (negative if applicable to any port)
* @param clientHost the host the authentication request is originating from; essentially, the computer name for
* this machine
* @param clientDomain the domain to authenticate within
*/
public void addNTLMProxyCredentials(final String username, final String password, final String host,
final int port, final String clientHost, final String clientDomain) {
final AuthScopeProxy scope = new AuthScopeProxy(host, port, AuthScope.ANY_REALM, AuthScope.ANY_SCHEME);
final Credentials c = new NTCredentialsExt(username, password, clientHost, clientDomain);
proxyCredentials_.put(scope, c);
clearAnswered(); // don't need to be precise, will cause in worst case one extra request
}
/**
* Returns the credentials associated with the specified scheme, host and port.
* @param scheme the authentication scheme being used (basic, digest, NTLM, etc)
* @param host the host we are authenticating for
* @param port the port we are authenticating for
* @param proxy Whether or not we are authenticating using a proxy
* @return the credentials corresponding to the specified scheme, host and port or <code>null</code>
* if already asked for it to avoid infinite loop
* @throws CredentialsNotAvailableException if the specified credentials cannot be provided due to an error
* @see CredentialsProvider#getCredentials(AuthScheme, String, int, boolean)
*/
public Credentials getCredentials(final AuthScheme scheme, final String host, final int port, final boolean proxy)
throws CredentialsNotAvailableException {
// It's the responsibility of the CredentialProvider to answer only once with a
// given Credentials to avoid infinite loop if it is incorrect:
// see http://issues.apache.org/bugzilla/show_bug.cgi?id=8140
if (alreadyAnswered(scheme, host, port, proxy)) {
LOG.debug("Already answered for " + buildKey(scheme, host, port, proxy) + ", returning null");
return null;
}
final Map<AuthScopeProxy, Credentials> credentials;
if (proxy) {
credentials = proxyCredentials_;
}
else {
credentials = credentials_;
}
for (final Map.Entry<AuthScopeProxy, Credentials> entry : credentials.entrySet()) {
final AuthScope scope = entry.getKey().getAuthScope();
final Credentials c = entry.getValue();
if (matchScheme(scope, scheme) && matchHost(scope, host)
&& matchPort(scope, port) && matchRealm(scope, scheme)) {
markAsAnswered(scheme, host, port, proxy);
LOG.debug("Returning " + c + " for " + buildKey(scheme, host, port, proxy));
return c;
}
}
LOG.debug("No credential found for " + buildKey(scheme, host, port, proxy));
return null;
}
/**
* @param scheme the request scheme for which Credentials are asked
* @param scope the configured authorization scope
* @return <code>true</code> if the scope's realm matches the one of the scheme
*/
protected boolean matchRealm(final AuthScope scope, final AuthScheme scheme) {
return scope.getRealm() == AuthScope.ANY_REALM || scope.getRealm().equals(scheme.getRealm());
}
/**
* @param port the request port for which Credentials are asked
* @param scope the configured authorization scope
* @return <code>true</code> if the scope's port matches the provided one
*/
protected boolean matchPort(final AuthScope scope, final int port) {
return scope.getPort() == AuthScope.ANY_PORT || scope.getPort() == port;
}
/**
* @param host the request host for which Credentials are asked
* @param scope the configured authorization scope
* @return <code>true</code> if the scope's host matches the provided one
*/
protected boolean matchHost(final AuthScope scope, final String host) {
return scope.getHost() == AuthScope.ANY_HOST || scope.getHost().equals(host);
}
/**
* @param scheme the request scheme for which Credentials are asked
* @param scope the configured authorization scope
* @return <code>true</code> if the scope's scheme matches the provided one
*/
protected boolean matchScheme(final AuthScope scope, final AuthScheme scheme) {
return scope.getScheme() == AuthScope.ANY_SCHEME || scope.getScheme().equals(scheme.getSchemeName());
}
/**
* Returns <tt>true</tt> if this provider has already provided an answer for the
* specified (scheme, host, port, proxy) combination.
* @param scheme the scheme
* @param host the server name
* @param port the server port
* @param proxy is proxy
* @return true if the provider has already provided an answer for this
*/
protected boolean alreadyAnswered(final AuthScheme scheme, final String host, final int port, final boolean proxy) {
return answerMarks_.contains(buildKey(scheme, host, port, proxy));
}
/**
* Marks the specified (scheme, host, port, proxy) combination as having already been processed.
* @param scheme the scheme
* @param host the server name
* @param port the server port
* @param proxy is proxy
*/
protected void markAsAnswered(final AuthScheme scheme, final String host, final int port, final boolean proxy) {
answerMarks_.add(buildKey(scheme, host, port, proxy));
}
/**
* Clears the cache of answered (scheme, host, port, proxy) combinations.
*/
protected void clearAnswered() {
answerMarks_.clear();
LOG.debug("Flushed marked answers");
}
/**
* Builds a key with the specified data.
* @param scheme the scheme
* @param host the server name
* @param port the server port
* @param proxy is proxy
* @return the new key
*/
protected Object buildKey(final AuthScheme scheme, final String host, final int port, final boolean proxy) {
return scheme.getSchemeName() + " " + scheme.getRealm() + " " + host + ":" + port + " " + proxy;
}
/**
* Clears the cache of answered credentials requests upon deserialization.
* @param stream the object stream containing the instance being deserialized
* @throws IOException if an IO error occurs
* @throws ClassNotFoundException if the class of a serialized object cannot be found
*/
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
answerMarks_.clear();
}
/**
* We have to wrap {@link AuthScope} instances in a serializable proxy so that the
* {@link DefaultCredentialsProvider} class can be serialized correctly.
*/
private static class AuthScopeProxy implements Serializable {
private static final long serialVersionUID = 1464373861677912537L;
private AuthScope authScope_;
public AuthScopeProxy(final String host, final int port, final String realm, final String scheme) {
authScope_ = new AuthScope(host, port, realm, scheme);
}
public AuthScope getAuthScope() {
return authScope_;
}
private void writeObject(final ObjectOutputStream stream) throws IOException {
stream.writeObject(authScope_.getHost());
stream.writeInt(authScope_.getPort());
stream.writeObject(authScope_.getRealm());
stream.writeObject(authScope_.getScheme());
}
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
final String host = (String) stream.readObject();
final int port = stream.readInt();
final String realm = (String) stream.readObject();
final String scheme = (String) stream.readObject();
authScope_ = new AuthScope(host, port, realm, scheme);
}
@Override
public int hashCode() {
return authScope_.hashCode();
}
@Override
public boolean equals(final Object obj) {
return obj instanceof AuthScopeProxy && authScope_.equals(((AuthScopeProxy) obj).getAuthScope());
}
}
/**
* We have to extend {@link UsernamePasswordCredentials} so that the
* {@link DefaultCredentialsProvider} class can be serialized correctly.
*/
private static class UsernamePasswordCredentialsExt
extends UsernamePasswordCredentials implements Serializable {
private static final long serialVersionUID = 6578356387067132849L;
public UsernamePasswordCredentialsExt(final String username, final String password) {
super(username, password);
}
private void writeObject(final ObjectOutputStream stream) throws IOException {
stream.writeObject(this.getUserName());
stream.writeObject(this.getPassword());
}
@SuppressWarnings("deprecation")
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
final String username = (String) stream.readObject();
final String password = (String) stream.readObject();
this.setUserName(username);
this.setPassword(password);
}
}
/**
* We have to extend {@link NTCredentials} so that the
* {@link DefaultCredentialsProvider} class can be serialized correctly.
*/
private static class NTCredentialsExt extends NTCredentials implements Serializable {
private static final long serialVersionUID = 1390068355010421017L;
public NTCredentialsExt(final String username, final String password, final String host, final String domain) {
super(username, password, host, domain);
}
private void writeObject(final ObjectOutputStream stream) throws IOException {
stream.writeObject(this.getUserName());
stream.writeObject(this.getPassword());
stream.writeObject(this.getHost());
stream.writeObject(this.getDomain());
}
@SuppressWarnings("deprecation")
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
final String username = (String) stream.readObject();
final String password = (String) stream.readObject();
final String host = (String) stream.readObject();
final String domain = (String) stream.readObject();
this.setUserName(username);
this.setPassword(password);
this.setHost(host);
this.setDomain(domain);
}
}
}
|