/**
 * 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.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 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.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.HftpFileSystem;
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
import org.apache.hadoop.hdfs.server.namenode.CancelDelegationTokenServlet;
import org.apache.hadoop.hdfs.server.namenode.GetDelegationTokenServlet;
import org.apache.hadoop.hdfs.server.namenode.NameNode;
import org.apache.hadoop.hdfs.server.namenode.RenewDelegationTokenServlet;
import org.apache.hadoop.io.IOUtils;
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.
 */
public class DelegationTokenFetcher {
  
  static{
    Configuration.addDefaultResource("hdfs-default.xml");
    Configuration.addDefaultResource("hdfs-site.xml");
  }

  private static final Log LOG = 
    LogFactory.getLog(DelegationTokenFetcher.class);
  private static final String WEBSERVICE = "webservice";
  private static final String CANCEL = "cancel";
  private static final String RENEW = "renew";
  
  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("  --cancel            Cancel the delegation token");
    err.println("  --renew             Renew 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 Configuration();
    Options fetcherOptions = new Options();
    fetcherOptions.addOption(WEBSERVICE, true, 
                             "HTTP/S url to reach the NameNode at");
    fetcherOptions.addOption(CANCEL, false, "cancel the token");
    fetcherOptions.addOption(RENEW, false, "renew 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 boolean cancel = cmd.hasOption(CANCEL);
    final boolean renew = cmd.hasOption(RENEW);
    String[] remaining = parser.getRemainingArgs();
    
    // check option validity
    if (cancel && renew) {
      System.err.println("ERROR: Only specify cancel or renew.");
      printUsage(System.err);
    }
    if (remaining.length != 1 || remaining[0].charAt(0) == '-') {
      System.err.println("ERROR: Must specify exactly 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
    final UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
    ugi.doAs(new PrivilegedExceptionAction<Object>() {
      @Override
      public Object run() throws Exception {
        
        if (cancel) {
          for(Token<?> token: readTokens(tokenFile, conf)) {
            if (token.isManaged()) {
              token.cancel(conf);
              System.out.println("Cancelled token for " + token.getService());
            }
          }          
        } else if (renew) {
          for(Token<?> token: readTokens(tokenFile, conf)) {
            if (token.isManaged()) {
              token.renew(conf);
              System.out.println("Renewed token for " + token.getService());
            }
          }          
        } else {
          if (webUrl != null) {
            getDTfromRemote(webUrl, null).
              writeTokenStorageFile(tokenFile, conf);
            System.out.println("Fetched token via http for " + webUrl);
          } else {
            FileSystem fs = FileSystem.get(conf);
            Token<?> token = fs.getDelegationToken(ugi.getShortUserName());
            Credentials cred = new Credentials();
            cred.addToken(token.getService(), token);
            cred.writeTokenStorageFile(tokenFile, conf);
            System.out.println("Fetched token for " + fs.getUri() + " into " +
                               tokenFile);
          }        
        }
        return null;
      }
    });

  }
  
  /**
   * Utility method to obtain a delegation token over http
   * @param nnAddr Namenode http addr, such as http://namenode:50070
   * @param renewer User that is renewing the ticket in such a request
   */
  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);
      }
      System.out.println("Retrieving token from: " + url);
      URL remoteURL = new URL(url.toString());
      URLConnection connection = SecurityUtil.openSecureHttpConnection(remoteURL);

      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());
      connection = (HttpURLConnection) SecurityUtil.openSecureHttpConnection(url);
      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 = null;
      if(connection != null) {
        String resp = connection.getResponseMessage();
        e = getExceptionFromResponse(resp);
      }
      
      IOUtils.cleanup(LOG, in);
      if(e!=null) {
        LOG.info("rethrowing exception from HTTP request: " + e.getLocalizedMessage());
        throw e;
      }
      throw ie;
    }
  }

  static private IOException getExceptionFromResponse(String resp) {
    String exceptionClass = "", exceptionMsg = "";
    if(resp != null && !resp.isEmpty()) {
      String[] rs = resp.split(";");
      exceptionClass = rs[0];
      exceptionMsg = rs[1];
    }
    LOG.info("Error response from HTTP request=" + resp + 
        ";ec=" + exceptionClass + ";em="+exceptionMsg);
    IOException e = null;
    if(exceptionClass != null  && !exceptionClass.isEmpty()) {
      if(exceptionClass.contains("InvalidToken")) {
        e = new org.apache.hadoop.security.token.SecretManager.InvalidToken(exceptionMsg);
        e.setStackTrace(new StackTraceElement[0]); // stack is not relevant
      } else if(exceptionClass.contains("AccessControlException")) {
        e = new org.apache.hadoop.security.AccessControlException(exceptionMsg);
        e.setStackTrace(new StackTraceElement[0]); // 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;
    try {
      URL url = new URL(buf.toString());
      HttpURLConnection connection =
              (HttpURLConnection)SecurityUtil.openSecureHttpConnection(url);
      if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
        throw new IOException("Error cancelling token:" + 
                              connection.getResponseMessage());
      }
    } catch (IOException ie) {
      IOUtils.cleanup(LOG, in);
      throw ie;
    }
  }
}
