blob: a43a7c9f9c660585d32a50d9ac54e4ea0390f232 [file] [log] [blame]
/**
* 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. See accompanying LICENSE file.
*/
package org.apache.hadoop.security.authentication.client;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* The {@link AuthenticatedURL} class enables the use of the JDK {@link URL} class
* against HTTP endpoints protected with the {@link AuthenticationFilter}.
* <p/>
* The authentication mechanisms supported by default are Hadoop Simple authentication
* (also known as pseudo authentication) and Kerberos SPNEGO authentication.
* <p/>
* Additional authentication mechanisms can be supported via {@link Authenticator} implementations.
* <p/>
* The default {@link Authenticator} is the {@link KerberosAuthenticator} class which supports
* automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication.
* <p/>
* <code>AuthenticatedURL</code> instances are not thread-safe.
* <p/>
* The usage pattern of the {@link AuthenticatedURL} is:
* <p/>
* <pre>
*
* // establishing an initial connection
*
* URL url = new URL("http://foo:8080/bar");
* AuthenticatedURL.Token token = new AuthenticatedURL.Token();
* AuthenticatedURL aUrl = new AuthenticatedURL();
* HttpURLConnection conn = new AuthenticatedURL(url, token).openConnection();
* ....
* // use the 'conn' instance
* ....
*
* // establishing a follow up connection using a token from the previous connection
*
* HttpURLConnection conn = new AuthenticatedURL(url, token).openConnection();
* ....
* // use the 'conn' instance
* ....
*
* </pre>
*/
public class AuthenticatedURL {
/**
* Name of the HTTP cookie used for the authentication token between the client and the server.
*/
public static final String AUTH_COOKIE = "hadoop.auth";
private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
/**
* Client side authentication token.
*/
public static class Token {
private String token;
/**
* Creates a token.
*/
public Token() {
}
/**
* Creates a token using an existing string representation of the token.
*
* @param tokenStr string representation of the tokenStr.
*/
public Token(String tokenStr) {
if (tokenStr == null) {
throw new IllegalArgumentException("tokenStr cannot be null");
}
set(tokenStr);
}
/**
* Returns if a token from the server has been set.
*
* @return if a token from the server has been set.
*/
public boolean isSet() {
return token != null;
}
/**
* Sets a token.
*
* @param tokenStr string representation of the tokenStr.
*/
void set(String tokenStr) {
token = tokenStr;
}
/**
* Returns the string representation of the token.
*
* @return the string representation of the token.
*/
@Override
public String toString() {
return token;
}
/**
* Return the hashcode for the token.
*
* @return the hashcode for the token.
*/
@Override
public int hashCode() {
return (token != null) ? token.hashCode() : 0;
}
/**
* Return if two token instances are equal.
*
* @param o the other token instance.
*
* @return if this instance and the other instance are equal.
*/
@Override
public boolean equals(Object o) {
boolean eq = false;
if (o instanceof Token) {
Token other = (Token) o;
eq = (token == null && other.token == null) || (token != null && this.token.equals(other.token));
}
return eq;
}
}
private static Class<? extends Authenticator> DEFAULT_AUTHENTICATOR = KerberosAuthenticator.class;
/**
* Sets the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance
* is created without specifying an authenticator.
*
* @param authenticator the authenticator class to use as default.
*/
public static void setDefaultAuthenticator(Class<? extends Authenticator> authenticator) {
DEFAULT_AUTHENTICATOR = authenticator;
}
/**
* Returns the default {@link Authenticator} class to use when an {@link AuthenticatedURL} instance
* is created without specifying an authenticator.
*
* @return the authenticator class to use as default.
*/
public static Class<? extends Authenticator> getDefaultAuthenticator() {
return DEFAULT_AUTHENTICATOR;
}
private Authenticator authenticator;
private ConnectionConfigurator connConfigurator;
/**
* Creates an {@link AuthenticatedURL}.
*/
public AuthenticatedURL() {
this(null);
}
/**
* Creates an <code>AuthenticatedURL</code>.
*
* @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link
* KerberosAuthenticator} is used.
*/
public AuthenticatedURL(Authenticator authenticator) {
this(authenticator, null);
}
/**
* Creates an <code>AuthenticatedURL</code>.
*
* @param authenticator the {@link Authenticator} instance to use, if <code>null</code> a {@link
* KerberosAuthenticator} is used.
* @param connConfigurator a connection configurator.
*/
public AuthenticatedURL(Authenticator authenticator,
ConnectionConfigurator connConfigurator) {
try {
this.authenticator = (authenticator != null) ? authenticator : DEFAULT_AUTHENTICATOR.newInstance();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
this.connConfigurator = connConfigurator;
this.authenticator.setConnectionConfigurator(connConfigurator);
}
/**
* Returns an authenticated {@link HttpURLConnection}.
*
* @param url the URL to connect to. Only HTTP/S URLs are supported.
* @param token the authentication token being used for the user.
*
* @return an authenticated {@link HttpURLConnection}.
*
* @throws IOException if an IO error occurred.
* @throws AuthenticationException if an authentication exception occurred.
*/
public HttpURLConnection openConnection(URL url, Token token) throws IOException, AuthenticationException {
if (url == null) {
throw new IllegalArgumentException("url cannot be NULL");
}
if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
throw new IllegalArgumentException("url must be for a HTTP or HTTPS resource");
}
if (token == null) {
throw new IllegalArgumentException("token cannot be NULL");
}
authenticator.authenticate(url, token);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (connConfigurator != null) {
conn = connConfigurator.configure(conn);
}
injectToken(conn, token);
return conn;
}
/**
* Helper method that injects an authentication token to send with a connection.
*
* @param conn connection to inject the authentication token into.
* @param token authentication token to inject.
*/
public static void injectToken(HttpURLConnection conn, Token token) {
String t = token.token;
if (t != null) {
if (!t.startsWith("\"")) {
t = "\"" + t + "\"";
}
conn.addRequestProperty("Cookie", AUTH_COOKIE_EQ + t);
}
}
/**
* Helper method that extracts an authentication token received from a connection.
* <p/>
* This method is used by {@link Authenticator} implementations.
*
* @param conn connection to extract the authentication token from.
* @param token the authentication token.
*
* @throws IOException if an IO error occurred.
* @throws AuthenticationException if an authentication exception occurred.
*/
public static void extractToken(HttpURLConnection conn, Token token) throws IOException, AuthenticationException {
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
Map<String, List<String>> headers = conn.getHeaderFields();
List<String> cookies = headers.get("Set-Cookie");
if (cookies != null) {
for (String cookie : cookies) {
if (cookie.startsWith(AUTH_COOKIE_EQ)) {
String value = cookie.substring(AUTH_COOKIE_EQ.length());
int separator = value.indexOf(";");
if (separator > -1) {
value = value.substring(0, separator);
}
if (value.length() > 0) {
token.set(value);
}
}
}
}
} else {
token.set(null);
throw new AuthenticationException("Authentication failed, status: " + conn.getResponseCode() +
", message: " + conn.getResponseMessage());
}
}
}