/**
 * 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.hdfs.tools;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
import java.util.Date;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.hdfs.HdfsConfiguration;
import org.apache.hadoop.hdfs.HftpFileSystem;
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenSecretManager;
import org.apache.hadoop.hdfs.server.namenode.CancelDelegationTokenServlet;
import org.apache.hadoop.hdfs.server.namenode.GetDelegationTokenServlet;
import org.apache.hadoop.hdfs.server.namenode.RenewDelegationTokenServlet;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.GenericOptionsParser;

/**
 * Fetch a DelegationToken from the current Namenode and store it in the
 * specified file.
 */
@InterfaceAudience.Private
public class DelegationTokenFetcher {
  private static final Log LOG = 
    LogFactory.getLog(DelegationTokenFetcher.class);
  private static final String WEBSERVICE = "webservice";
  private static final String RENEWER = "renewer";
  private static final String CANCEL = "cancel";
  private static final String RENEW = "renew";
  private static final String PRINT = "print";

  static {
    // Enable Kerberos sockets
    System.setProperty("https.cipherSuites", "TLS_KRB5_WITH_3DES_EDE_CBC_SHA");
  }

  private static void printUsage(PrintStream err) throws IOException {
    err.println("fetchdt retrieves delegation tokens from the NameNode");
    err.println();
    err.println("fetchdt <opts> <token file>");
    err.println("Options:");
    err.println("  --webservice <url>  Url to contact NN on");
    err.println("  --renewer <name>    Name of the delegation token renewer");
    err.println("  --cancel            Cancel the delegation token");
    err.println("  --renew             Renew the delegation token.  Delegation " 
    		+ "token must have been fetched using the --renewer <name> option.");
    err.println("  --print             Print the delegation token");
    err.println();
    GenericOptionsParser.printGenericCommandUsage(err);
    System.exit(1);
  }

  private static Collection<Token<?>> readTokens(Path file, Configuration conf)
      throws IOException {
    Credentials creds = Credentials.readTokenStorageFile(file, conf);
    return creds.getAllTokens();
  }
    
  /**
   * Command-line interface
   */
  public static void main(final String[] args) throws Exception {
    final Configuration conf = new HdfsConfiguration();
    Options fetcherOptions = new Options();
    fetcherOptions.addOption(WEBSERVICE, true,
        "HTTPS url to reach the NameNode at");
    fetcherOptions.addOption(RENEWER, true,
        "Name of the delegation token renewer");
    fetcherOptions.addOption(CANCEL, false, "cancel the token");
    fetcherOptions.addOption(RENEW, false, "renew the token");
    fetcherOptions.addOption(PRINT, false, "print the token");
    GenericOptionsParser parser = new GenericOptionsParser(conf,
        fetcherOptions, args);
    CommandLine cmd = parser.getCommandLine();
    
    // get options
    final String webUrl = cmd.hasOption(WEBSERVICE) ? cmd
        .getOptionValue(WEBSERVICE) : null;
    final String renewer = cmd.hasOption(RENEWER) ? 
        cmd.getOptionValue(RENEWER) : null;
    final boolean cancel = cmd.hasOption(CANCEL);
    final boolean renew = cmd.hasOption(RENEW);
    final boolean print = cmd.hasOption(PRINT);
    String[] remaining = parser.getRemainingArgs();

    // check option validity
    if (cancel && renew || cancel && print || renew && print || cancel && renew
        && print) {
      System.err.println("ERROR: Only specify cancel, renew or print.");
      printUsage(System.err);
    }
    if (remaining.length != 1 || remaining[0].charAt(0) == '-') {
      System.err.println("ERROR: Must specify exacltly one token file");
      printUsage(System.err);
    }
    // default to using the local file system
    FileSystem local = FileSystem.getLocal(conf);
    final Path tokenFile = new Path(local.getWorkingDirectory(), remaining[0]);

    // Login the current user
    UserGroupInformation.getCurrentUser().doAs(
        new PrivilegedExceptionAction<Object>() {
          @SuppressWarnings("unchecked")
          @Override
          public Object run() throws Exception {
            if (print) {
              DelegationTokenIdentifier id = new DelegationTokenSecretManager(
                  0, 0, 0, 0, null).createIdentifier();
              for (Token<?> token : readTokens(tokenFile, conf)) {
                DataInputStream in = new DataInputStream(
                    new ByteArrayInputStream(token.getIdentifier()));
                id.readFields(in);
                System.out.println("Token (" + id + ") for " + token.getService());
              }
              return null;
            }
            
            if (renew) {
              for (Token<?> token : readTokens(tokenFile, conf)) {
                if (token.isManaged()) {
                  long result = token.renew(conf);
                  if (LOG.isDebugEnabled()) {
                    LOG.debug("Renewed token for " + token.getService()
                        + " until: " + new Date(result));
                  }
                }
              }
            } else if (cancel) {
              for(Token<?> token: readTokens(tokenFile, conf)) {
                if (token.isManaged()) {
                  token.cancel(conf);
                  if (LOG.isDebugEnabled()) {
                    LOG.debug("Cancelled token for " + token.getService());
                  }
                }
              }
            } else {
              // otherwise we are fetching
              if (webUrl != null) {
                Credentials creds = getDTfromRemote(webUrl, renewer);
                creds.writeTokenStorageFile(tokenFile, conf);
                for (Token<?> token : creds.getAllTokens()) {
                  System.out.println("Fetched token via " + webUrl + " for "
                      + token.getService() + " into " + tokenFile);
                }
              } else {
                FileSystem fs = FileSystem.get(conf);
                Token<?> token = fs.getDelegationToken(renewer);
                Credentials cred = new Credentials();
                cred.addToken(token.getService(), token);
                cred.writeTokenStorageFile(tokenFile, conf);
                System.out.println("Fetched token for " + token.getService()
                    + " into " + tokenFile);
              }
            }
            return null;
          }
        });
  }
  
  static public Credentials getDTfromRemote(String nnAddr, 
      String renewer) throws IOException {
    DataInputStream dis = null;
    InetSocketAddress serviceAddr = NetUtils.createSocketAddr(nnAddr);
    
    try {
      StringBuffer url = new StringBuffer();
      if (renewer != null) {
        url.append(nnAddr).append(GetDelegationTokenServlet.PATH_SPEC)
           .append("?").append(GetDelegationTokenServlet.RENEWER).append("=")
           .append(renewer);
      } else {
        url.append(nnAddr).append(GetDelegationTokenServlet.PATH_SPEC);
      }
      URL remoteURL = new URL(url.toString());
      SecurityUtil.fetchServiceTicket(remoteURL);
      URLConnection connection = remoteURL.openConnection();

      InputStream in = connection.getInputStream();
      Credentials ts = new Credentials();
      dis = new DataInputStream(in);
      ts.readFields(dis);
      for(Token<?> token: ts.getAllTokens()) {
        token.setKind(HftpFileSystem.TOKEN_KIND);
        SecurityUtil.setTokenService(token, serviceAddr);
      }
      return ts;
    } catch (Exception e) {
      throw new IOException("Unable to obtain remote token", e);
    } finally {
      if(dis != null) dis.close();
    }
  }

  /**
   * Renew a Delegation Token.
   * @param nnAddr the NameNode's address
   * @param tok the token to renew
   * @return the Date that the token will expire next.
   * @throws IOException
   */
  static public long renewDelegationToken(String nnAddr,
      Token<DelegationTokenIdentifier> tok
  ) throws IOException {
    StringBuilder buf = new StringBuilder();
    buf.append(nnAddr);
    buf.append(RenewDelegationTokenServlet.PATH_SPEC);
    buf.append("?");
    buf.append(RenewDelegationTokenServlet.TOKEN);
    buf.append("=");
    buf.append(tok.encodeToUrlString());
    BufferedReader in = null;
    HttpURLConnection connection = null;
    
    try {
      URL url = new URL(buf.toString());
      SecurityUtil.fetchServiceTicket(url);
      connection = (HttpURLConnection) url.openConnection();
      if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
        throw new IOException("Error renewing token: " + 
            connection.getResponseMessage());
      }
      in = new BufferedReader(new InputStreamReader
          (connection.getInputStream()));
      long result = Long.parseLong(in.readLine());
      in.close();
      return result;
    } catch (IOException ie) {
      LOG.info("error in renew over HTTP", ie);
      IOException e = getExceptionFromResponse(connection);

      IOUtils.cleanup(LOG, in);
      if(e!=null) {
        LOG.info("rethrowing exception from HTTP request: " + 
                 e.getLocalizedMessage());
        throw e;
      }
      throw ie;
    }
  }

  // parse the message and extract the name of the exception and the message
  static private IOException getExceptionFromResponse(HttpURLConnection con) {
    IOException e = null;
    String resp;
    if(con == null) 
      return null;    
    
    try {
      resp = con.getResponseMessage();
    } catch (IOException ie) { return null; }
    if(resp == null || resp.isEmpty())
      return null;

    String exceptionClass = "", exceptionMsg = "";
    String[] rs = resp.split(";");
    if(rs.length < 2)
      return null;
    exceptionClass = rs[0];
    exceptionMsg = rs[1];
    LOG.info("Error response from HTTP request=" + resp + 
        ";ec=" + exceptionClass + ";em="+exceptionMsg);
    
    if(exceptionClass == null || exceptionClass.isEmpty())
      return null;
    
    // recreate exception objects
    try {
      Class<? extends Exception> ec = 
         Class.forName(exceptionClass).asSubclass(Exception.class);
      // we are interested in constructor with String arguments
      java.lang.reflect.Constructor<? extends Exception> constructor =
        (java.lang.reflect.Constructor<? extends Exception>) 
        ec.getConstructor (new Class[] {String.class});

      // create an instance
      e =  (IOException) constructor.newInstance (exceptionMsg);

    } catch (Exception ee)  {
      LOG.warn("failed to create object of this class", ee);
    }
    if(e == null)
      return null;
    
    e.setStackTrace(new StackTraceElement[0]); // local stack is not relevant
    LOG.info("Exception from HTTP response=" + e.getLocalizedMessage());
    return e;
  }

  
  /**
   * Cancel a Delegation Token.
   * @param nnAddr the NameNode's address
   * @param tok the token to cancel
   * @throws IOException
   */
  static public void cancelDelegationToken(String nnAddr,
      Token<DelegationTokenIdentifier> tok
  ) throws IOException {
    StringBuilder buf = new StringBuilder();
    buf.append(nnAddr);
    buf.append(CancelDelegationTokenServlet.PATH_SPEC);
    buf.append("?");
    buf.append(CancelDelegationTokenServlet.TOKEN);
    buf.append("=");
    buf.append(tok.encodeToUrlString());
    BufferedReader in = null;
    HttpURLConnection connection=null;
    try {
      URL url = new URL(buf.toString());
      SecurityUtil.fetchServiceTicket(url);
      connection = (HttpURLConnection) url.openConnection();
      if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
        throw new IOException("Error cancelling token: " + 
            connection.getResponseMessage());
      }
    } catch (IOException ie) {
      LOG.info("error in cancel over HTTP", ie);
      IOException e = getExceptionFromResponse(connection);

      IOUtils.cleanup(LOG, in);
      if(e!=null) {
        LOG.info("rethrowing exception from HTTP request: " + 
                 e.getLocalizedMessage());
        throw e;
      }
      throw ie;
    }
  }
}
