/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.security.token.delegation.web;

import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.client.ConnectionConfigurator;
import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * The <code>DelegationTokenAuthenticatedURL</code> is a
 * {@link AuthenticatedURL} sub-class with built-in Hadoop Delegation Token
 * functionality.
 * <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
 * DelegationTokenAuthenticator} implementations.
 * <p>
 * The default {@link DelegationTokenAuthenticator} is the {@link
 * KerberosDelegationTokenAuthenticator} class which supports
 * automatic fallback from Kerberos SPNEGO to Hadoop Simple authentication via
 * the {@link PseudoDelegationTokenAuthenticator} class.
 * <p>
 * <code>AuthenticatedURL</code> instances are not thread-safe.
 */
@InterfaceAudience.Public
@InterfaceStability.Unstable
public class DelegationTokenAuthenticatedURL extends AuthenticatedURL {

  private static final Logger LOG =
      LoggerFactory.getLogger(DelegationTokenAuthenticatedURL.class);

  /**
   * Constant used in URL's query string to perform a proxy user request, the
   * value of the <code>DO_AS</code> parameter is the user the request will be
   * done on behalf of.
   */
  static final String DO_AS = "doAs";

  /**
   * Client side authentication token that handles Delegation Tokens.
   */
  @InterfaceAudience.Public
  @InterfaceStability.Unstable
  public static class Token extends AuthenticatedURL.Token {
    private
    org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
        delegationToken;

    public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
    getDelegationToken() {
      return delegationToken;
    }
    public void setDelegationToken(
        org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier> delegationToken) {
      this.delegationToken = delegationToken;
    }

  }

  private static Class<? extends DelegationTokenAuthenticator>
      DEFAULT_AUTHENTICATOR = KerberosDelegationTokenAuthenticator.class;

  /**
   * Sets the default {@link DelegationTokenAuthenticator} class to use when an
   * {@link DelegationTokenAuthenticatedURL} instance is created without
   * specifying one.
   *
   * The default class is {@link KerberosDelegationTokenAuthenticator}
   *
   * @param authenticator the authenticator class to use as default.
   */
  public static void setDefaultDelegationTokenAuthenticator(
      Class<? extends DelegationTokenAuthenticator> authenticator) {
    DEFAULT_AUTHENTICATOR = authenticator;
  }

  /**
   * Returns the default {@link DelegationTokenAuthenticator} class to use when
   * an {@link DelegationTokenAuthenticatedURL} instance is created without
   * specifying one.
   * <p>
   * The default class is {@link KerberosDelegationTokenAuthenticator}
   *
   * @return the delegation token authenticator class to use as default.
   */
  public static Class<? extends DelegationTokenAuthenticator>
      getDefaultDelegationTokenAuthenticator() {
    return DEFAULT_AUTHENTICATOR;
  }

  private static DelegationTokenAuthenticator
      obtainDelegationTokenAuthenticator(DelegationTokenAuthenticator dta,
            ConnectionConfigurator connConfigurator) {
    try {
      if (dta == null) {
        dta = DEFAULT_AUTHENTICATOR.newInstance();
        dta.setConnectionConfigurator(connConfigurator);
      }
      return dta;
    } catch (Exception ex) {
      throw new IllegalArgumentException(ex);
    }
  }

  private boolean useQueryStringforDelegationToken = false;

  /**
   * Creates an <code>DelegationTokenAuthenticatedURL</code>.
   * <p>
   * An instance of the default {@link DelegationTokenAuthenticator} will be
   * used.
   */
  public DelegationTokenAuthenticatedURL() {
    this(null, null);
  }

  /**
   * Creates an <code>DelegationTokenAuthenticatedURL</code>.
   *
   * @param authenticator the {@link DelegationTokenAuthenticator} instance to
   * use, if <code>null</code> the default one will be used.
   */
  public DelegationTokenAuthenticatedURL(
      DelegationTokenAuthenticator authenticator) {
    this(authenticator, null);
  }

  /**
   * Creates an <code>DelegationTokenAuthenticatedURL</code> using the default
   * {@link DelegationTokenAuthenticator} class.
   *
   * @param connConfigurator a connection configurator.
   */
  public DelegationTokenAuthenticatedURL(
      ConnectionConfigurator connConfigurator) {
    this(null, connConfigurator);
  }

  /**
   * Creates an <code>DelegationTokenAuthenticatedURL</code>.
   *
   * @param authenticator the {@link DelegationTokenAuthenticator} instance to
   * use, if <code>null</code> the default one will be used.
   * @param connConfigurator a connection configurator.
   */
  public DelegationTokenAuthenticatedURL(
      DelegationTokenAuthenticator authenticator,
      ConnectionConfigurator connConfigurator) {
    super(obtainDelegationTokenAuthenticator(authenticator, connConfigurator),
            connConfigurator);
  }

  /**
   * Sets if delegation token should be transmitted in the URL query string.
   * By default it is transmitted using the
   * {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP header.
   * <p>
   * This method is provided to enable WebHDFS backwards compatibility.
   *
   * @param useQueryString  <code>TRUE</code> if the token is transmitted in the
   * URL query string, <code>FALSE</code> if the delegation token is transmitted
   * using the {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP
   * header.
   */
  @Deprecated
  protected void setUseQueryStringForDelegationToken(boolean useQueryString) {
    useQueryStringforDelegationToken = useQueryString;
  }

  /**
   * Returns if delegation token is transmitted as a HTTP header.
   *
   * @return <code>TRUE</code> if the token is transmitted in the URL query
   * string, <code>FALSE</code> if the delegation token is transmitted using the
   * {@link DelegationTokenAuthenticator#DELEGATION_TOKEN_HEADER} HTTP header.
   */
  public boolean useQueryStringForDelegationToken() {
    return useQueryStringforDelegationToken;
  }

  /**
   * Returns an authenticated {@link HttpURLConnection}, it uses a Delegation
   * Token only if the given auth token is an instance of {@link Token} and
   * it contains a Delegation Token, otherwise use the configured
   * {@link DelegationTokenAuthenticator} to authenticate the connection.
   *
   * @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.
   */
  @Override
  public HttpURLConnection openConnection(URL url, AuthenticatedURL.Token token)
      throws IOException, AuthenticationException {
    return (token instanceof Token) ? openConnection(url, (Token) token)
                                    : super.openConnection(url ,token);
  }

  /**
   * Returns an authenticated {@link HttpURLConnection}. If the Delegation
   * Token is present, it will be used taking precedence over the configured
   * <code>Authenticator</code>.
   *
   * @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 {
    return openConnection(url, token, null);
  }

  private URL augmentURL(URL url, Map<String, String> params)
      throws IOException {
    if (params != null && params.size() > 0) {
      String urlStr = url.toExternalForm();
      StringBuilder sb = new StringBuilder(urlStr);
      String separator = (urlStr.contains("?")) ? "&" : "?";
      for (Map.Entry<String, String> param : params.entrySet()) {
        sb.append(separator).append(param.getKey()).append("=").append(
            param.getValue());
        separator = "&";
      }
      url = new URL(sb.toString());
    }
    return url;
  }

  /**
   * Returns an authenticated {@link HttpURLConnection}. If the Delegation
   * Token is present, it will be used taking precedence over the configured
   * <code>Authenticator</code>. If the <code>doAs</code> parameter is not NULL,
   * the request will be done on behalf of the specified <code>doAs</code> user.
   *
   * @param url the URL to connect to. Only HTTP/S URLs are supported.
   * @param token the authentication token being used for the user.
   * @param doAs user to do the the request on behalf of, if NULL the request is
   * as self.
   * @return an authenticated {@link HttpURLConnection}.
   * @throws IOException if an IO error occurred.
   * @throws AuthenticationException if an authentication exception occurred.
   */
  @SuppressWarnings("unchecked")
  public HttpURLConnection openConnection(URL url, Token token, String doAs)
      throws IOException, AuthenticationException {
    Preconditions.checkNotNull(url, "url");
    Preconditions.checkNotNull(token, "token");
    Map<String, String> extraParams = new HashMap<String, String>();
    org.apache.hadoop.security.token.Token<? extends TokenIdentifier> dToken
        = null;
    LOG.debug("Connecting to url {} with token {} as {}", url, token, doAs);
    // if we have valid auth token, it takes precedence over a delegation token
    // and we don't even look for one.
    if (!token.isSet()) {
      // delegation token
      Credentials creds = UserGroupInformation.getCurrentUser().
          getCredentials();
      LOG.debug("Token not set, looking for delegation token. Creds:{},"
          + " size:{}", creds.getAllTokens(), creds.numberOfTokens());
      if (!creds.getAllTokens().isEmpty()) {
        dToken = selectDelegationToken(url, creds);
        if (dToken != null) {
          if (useQueryStringForDelegationToken()) {
            // delegation token will go in the query string, injecting it
            extraParams.put(
                KerberosDelegationTokenAuthenticator.DELEGATION_PARAM,
                dToken.encodeToUrlString());
          } else {
            // delegation token will go as request header, setting it in the
            // auth-token to ensure no authentication handshake is triggered
            // (if we have a delegation token, we are authenticated)
            // the delegation token header is injected in the connection request
            // at the end of this method.
            token.delegationToken = (org.apache.hadoop.security.token.Token
                <AbstractDelegationTokenIdentifier>) dToken;
          }
        }
      }
    }

    // proxyuser
    if (doAs != null) {
      extraParams.put(DO_AS, URLEncoder.encode(doAs, "UTF-8"));
    }

    url = augmentURL(url, extraParams);
    HttpURLConnection conn = super.openConnection(url, token);
    if (!token.isSet() && !useQueryStringForDelegationToken() && dToken != null) {
      // injecting the delegation token header in the connection request
      conn.setRequestProperty(
          DelegationTokenAuthenticator.DELEGATION_TOKEN_HEADER,
          dToken.encodeToUrlString());
    }
    return conn;
  }

  /**
   * Select a delegation token from all tokens in credentials, based on url.
   *
   * @param url url.
   * @param creds credentials.
   * @return token.
   */
  @InterfaceAudience.Private
  public org.apache.hadoop.security.token.Token<? extends TokenIdentifier>
      selectDelegationToken(URL url, Credentials creds) {
    final InetSocketAddress serviceAddr = new InetSocketAddress(url.getHost(),
        url.getPort());
    final Text service = SecurityUtil.buildTokenService(serviceAddr);
    org.apache.hadoop.security.token.Token<? extends TokenIdentifier> dToken =
        creds.getToken(service);
    LOG.debug("Using delegation token {} from service:{}", dToken, service);
    return dToken;
  }

  /**
   * Requests a delegation token using the configured <code>Authenticator</code>
   * for authentication.
   *
   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
   * supported.
   * @param token the authentication token being used for the user where the
   * Delegation token will be stored.
   * @param renewer the renewer user.
   * @return a delegation token.
   * @throws IOException if an IO error occurred.
   * @throws AuthenticationException if an authentication exception occurred.
   */
  public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
      getDelegationToken(URL url, Token token, String renewer)
          throws IOException, AuthenticationException {
    return getDelegationToken(url, token, renewer, null);
  }

  /**
   * Requests a delegation token using the configured <code>Authenticator</code>
   * for authentication.
   *
   * @param url the URL to get the delegation token from. Only HTTP/S URLs are
   * supported.
   * @param token the authentication token being used for the user where the
   * Delegation token will be stored.
   * @param renewer the renewer user.
   * @param doAsUser the user to do as, which will be the token owner.
   * @return a delegation token.
   * @throws IOException if an IO error occurred.
   * @throws AuthenticationException if an authentication exception occurred.
   */
  public org.apache.hadoop.security.token.Token<AbstractDelegationTokenIdentifier>
      getDelegationToken(URL url, Token token, String renewer, String doAsUser)
          throws IOException, AuthenticationException {
    Preconditions.checkNotNull(url, "url");
    Preconditions.checkNotNull(token, "token");
    try {
      token.delegationToken =
          ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
              getDelegationToken(url, token, renewer, doAsUser);
      return token.delegationToken;
    } catch (IOException ex) {
      token.delegationToken = null;
      throw ex;
    }
  }

  /**
   * Renews a delegation token from the server end-point using the
   * configured <code>Authenticator</code> for authentication.
   *
   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
   * supported.
   * @param token the authentication token with the Delegation Token to renew.
   * @throws IOException if an IO error occurred.
   * @throws AuthenticationException if an authentication exception occurred.
   * @return delegation token long value.
   */
  public long renewDelegationToken(URL url, Token token)
      throws IOException, AuthenticationException {
    return renewDelegationToken(url, token, null);
  }

  /**
   * Renews a delegation token from the server end-point using the
   * configured <code>Authenticator</code> for authentication.
   *
   * @param url the URL to renew the delegation token from. Only HTTP/S URLs are
   * supported.
   * @param token the authentication token with the Delegation Token to renew.
   * @param doAsUser the user to do as, which will be the token owner.
   * @throws IOException if an IO error occurred.
   * @throws AuthenticationException if an authentication exception occurred.
   * @return delegation token long value.
   */
  public long renewDelegationToken(URL url, Token token, String doAsUser)
      throws IOException, AuthenticationException {
    Preconditions.checkNotNull(url, "url");
    Preconditions.checkNotNull(token, "token");
    Preconditions.checkNotNull(token.delegationToken,
        "No delegation token available");
    try {
      return ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
          renewDelegationToken(url, token, token.delegationToken, doAsUser);
    } catch (IOException ex) {
      token.delegationToken = null;
      throw ex;
    }
  }

  /**
   * Cancels a delegation token from the server end-point. It does not require
   * being authenticated by the configured <code>Authenticator</code>.
   *
   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
   * are supported.
   * @param token the authentication token with the Delegation Token to cancel.
   * @throws IOException if an IO error occurred.
   */
  public void cancelDelegationToken(URL url, Token token)
      throws IOException {
    cancelDelegationToken(url, token, null);
  }

  /**
   * Cancels a delegation token from the server end-point. It does not require
   * being authenticated by the configured <code>Authenticator</code>.
   *
   * @param url the URL to cancel the delegation token from. Only HTTP/S URLs
   * are supported.
   * @param token the authentication token with the Delegation Token to cancel.
   * @param doAsUser the user to do as, which will be the token owner.
   * @throws IOException if an IO error occurred.
   */
  public void cancelDelegationToken(URL url, Token token, String doAsUser)
      throws IOException {
    Preconditions.checkNotNull(url, "url");
    Preconditions.checkNotNull(token, "token");
    Preconditions.checkNotNull(token.delegationToken,
        "No delegation token available");
    try {
      ((KerberosDelegationTokenAuthenticator) getAuthenticator()).
          cancelDelegationToken(url, token, token.delegationToken, doAsUser);
    } finally {
      token.delegationToken = null;
    }
  }

}
