Java tutorial
/** * 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileNotFoundException; import java.io.IOException; import java.net.CookieHandler; import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; 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: * <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().openConnection(url, token); * .... * // use the 'conn' instance * .... * * // establishing a follow up connection using a token from the previous connection * * HttpURLConnection conn = new AuthenticatedURL().openConnection(url, token); * .... * // use the 'conn' instance * .... * * </pre> */ public class AuthenticatedURL { private static final Logger LOG = LoggerFactory.getLogger(AuthenticatedURL.class); /** * Name of the HTTP cookie used for the authentication token between the client and the server. */ public static final String AUTH_COOKIE = "hadoop.auth"; // a lightweight cookie handler that will be attached to url connections. // client code is not required to extract or inject auth cookies. private static class AuthCookieHandler extends CookieHandler { private HttpCookie authCookie; private Map<String, List<String>> cookieHeaders = Collections.emptyMap(); @Override public synchronized Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) throws IOException { // call getter so it will reset headers if token is expiring. getAuthCookie(); return cookieHeaders; } @Override public void put(URI uri, Map<String, List<String>> responseHeaders) { List<String> headers = responseHeaders.get("Set-Cookie"); if (headers != null) { for (String header : headers) { List<HttpCookie> cookies; try { cookies = HttpCookie.parse(header); } catch (IllegalArgumentException iae) { // don't care. just skip malformed cookie headers. LOG.debug("Cannot parse cookie header: " + header, iae); continue; } for (HttpCookie cookie : cookies) { if (AUTH_COOKIE.equals(cookie.getName())) { setAuthCookie(cookie); } } } } } // return the auth cookie if still valid. private synchronized HttpCookie getAuthCookie() { if (authCookie != null && authCookie.hasExpired()) { setAuthCookie(null); } return authCookie; } private synchronized void setAuthCookie(HttpCookie cookie) { final HttpCookie oldCookie = authCookie; // will redefine if new cookie is valid. authCookie = null; cookieHeaders = Collections.emptyMap(); boolean valid = cookie != null && !cookie.getValue().isEmpty() && !cookie.hasExpired(); if (valid) { // decrease lifetime to avoid using a cookie soon to expire. // allows authenticators to pre-emptively reauthenticate to // prevent clients unnecessarily receiving a 401. long maxAge = cookie.getMaxAge(); if (maxAge != -1) { cookie.setMaxAge(maxAge * 9 / 10); valid = !cookie.hasExpired(); } } if (valid) { // v0 cookies value aren't quoted by default but tomcat demands // quoting. if (cookie.getVersion() == 0) { String value = cookie.getValue(); if (!value.startsWith("\"")) { value = "\"" + value + "\""; cookie.setValue(value); } } authCookie = cookie; cookieHeaders = new HashMap<>(); cookieHeaders.put("Cookie", Arrays.asList(cookie.toString())); } LOG.trace("Setting token value to {} ({})", authCookie, oldCookie); } private void setAuthCookieValue(String value) { HttpCookie c = null; if (value != null) { c = new HttpCookie(AUTH_COOKIE, value); } setAuthCookie(c); } } /** * Client side authentication token. */ public static class Token { private final AuthCookieHandler cookieHandler = new AuthCookieHandler(); /** * 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 cookieHandler.getAuthCookie() != null; } /** * Sets a token. * * @param tokenStr string representation of the tokenStr. */ void set(String tokenStr) { cookieHandler.setAuthCookieValue(tokenStr); } /** * Installs a cookie handler for the http request to manage session * cookies. * @param url * @return HttpUrlConnection * @throws IOException */ HttpURLConnection openConnection(URL url, ConnectionConfigurator connConfigurator) throws IOException { // the cookie handler is unfortunately a global static. it's a // synchronized class method so we can safely swap the handler while // instantiating the connection object to prevent it leaking into // other connections. final HttpURLConnection conn; synchronized (CookieHandler.class) { CookieHandler current = CookieHandler.getDefault(); CookieHandler.setDefault(cookieHandler); try { conn = (HttpURLConnection) url.openConnection(); } finally { CookieHandler.setDefault(current); } } if (connConfigurator != null) { connConfigurator.configure(conn); } return conn; } /** * Returns the string representation of the token. * * @return the string representation of the token. */ @Override public String toString() { String value = ""; HttpCookie authCookie = cookieHandler.getAuthCookie(); if (authCookie != null) { value = authCookie.getValue(); if (value.startsWith("\"")) { // tests don't want the quotes. value = value.substring(1, value.length() - 1); } } return value; } } 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 the {@link Authenticator} instance used by the * <code>AuthenticatedURL</code>. * * @return the {@link Authenticator} instance */ protected Authenticator getAuthenticator() { return authenticator; } /** * 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); // allow the token to create the connection with a cookie handler for // managing session cookies. return token.openConnection(url, connConfigurator); } /** * Helper method that injects an authentication token to send with a * connection. Callers should prefer using * {@link Token#openConnection(URL, ConnectionConfigurator)} which * automatically manages authentication tokens. * * @param conn connection to inject the authentication token into. * @param token authentication token to inject. */ public static void injectToken(HttpURLConnection conn, Token token) { HttpCookie authCookie = token.cookieHandler.getAuthCookie(); if (authCookie != null) { conn.addRequestProperty("Cookie", authCookie.toString()); } } /** * 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 { int respCode = conn.getResponseCode(); if (respCode == HttpURLConnection.HTTP_OK || respCode == HttpURLConnection.HTTP_CREATED || respCode == HttpURLConnection.HTTP_ACCEPTED) { // cookie handler should have already extracted the token. try again // for backwards compatibility if this method is called on a connection // not opened via this instance. token.cookieHandler.put(null, conn.getHeaderFields()); } else if (respCode == HttpURLConnection.HTTP_NOT_FOUND) { LOG.trace("Setting token value to null ({}), resp={}", token, respCode); token.set(null); throw new FileNotFoundException(conn.getURL().toString()); } else { LOG.trace("Setting token value to null ({}), resp={}", token, respCode); token.set(null); throw new AuthenticationException("Authentication failed" + ", URL: " + conn.getURL() + ", status: " + conn.getResponseCode() + ", message: " + conn.getResponseMessage()); } } }