/** * 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()); } } }