/**
* 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.URI;
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.DFSConfigKeys;
import org.apache.hadoop.hdfs.DFSUtil;
import org.apache.hadoop.hdfs.HftpFileSystem;
import org.apache.hadoop.hdfs.HsftpFileSystem;
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.Krb5AndCertsSslSocketConnector;
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 {
// reference a field to make sure the static blocks run
int x = Krb5AndCertsSslSocketConnector.KRB5_CIPHER_SUITES.size();
}
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();
setupSsl(conf);
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]);
if (cancel) {
for(Token<?> token: readTokens(tokenFile, conf)) {
if (token.isManaged()) {
token.cancel(conf);
}
}
} else if (renew) {
for(Token<?> token: readTokens(tokenFile, conf)) {
if (token.isManaged()) {
token.renew(conf);
}
}
} else {
if (webUrl != null) {
URI uri = new URI(webUrl);
getDTfromRemote(uri.getScheme(),
new InetSocketAddress(uri.getHost(), uri.getPort()),
null,
conf).
writeTokenStorageFile(tokenFile, conf);
} else {
FileSystem fs = FileSystem.get(conf);
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
Token<?> token = fs.getDelegationToken(ugi.getShortUserName());
Credentials cred = new Credentials();
cred.addToken(token.getService(), token);
cred.writeTokenStorageFile(tokenFile, conf);
if (LOG.isDebugEnabled()) {
LOG.debug("Fetched token for " + fs.getUri() + " into " +
tokenFile);
}
}
}
}
/** Set up SSL resources */
public static void setupSsl(Configuration conf) {
Configuration sslConf = new Configuration(false);
sslConf.addResource(conf.get("dfs.https.client.keystore.resource",
"ssl-client.xml"));
System.setProperty("javax.net.ssl.trustStore", sslConf.get(
"ssl.client.truststore.location", ""));
System.setProperty("javax.net.ssl.trustStorePassword", sslConf.get(
"ssl.client.truststore.password", ""));
System.setProperty("javax.net.ssl.trustStoreType", sslConf.get(
"ssl.client.truststore.type", "jks"));
System.setProperty("javax.net.ssl.keyStore", sslConf.get(
"ssl.client.keystore.location", ""));
System.setProperty("javax.net.ssl.keyStorePassword", sslConf.get(
"ssl.client.keystore.password", ""));
System.setProperty("javax.net.ssl.keyPassword", sslConf.get(
"ssl.client.keystore.keypassword", ""));
System.setProperty("javax.net.ssl.keyStoreType", sslConf.get(
"ssl.client.keystore.type", "jks"));
}
/**
* Utility method to obtain a delegation token over http
* @param protocol whether to use http or https
* @param nnAddr the address for the NameNode
* @param renewer User that is renewing the ticket in such a request
* @param conf the configuration
*/
static public Credentials getDTfromRemote(String protocol,
final InetSocketAddress nnAddr,
String renewer,
Configuration conf
) throws IOException {
final String renewAddress = getRenewAddress(protocol, nnAddr, conf);
final boolean https = "https".equals(protocol);
try {
StringBuffer url = new StringBuffer(renewAddress);
url.append(GetDelegationTokenServlet.PATH_SPEC);
if (renewer != null) {
url.append("?").
append(GetDelegationTokenServlet.RENEWER).append("=").
append(renewer);
}
if(LOG.isDebugEnabled()) {
LOG.debug("Retrieving token from: " + url);
}
final URL remoteURL = new URL(url.toString());
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
return ugi.doAs(new PrivilegedExceptionAction<Credentials>(){
public Credentials run() throws Exception {
URLConnection connection =
SecurityUtil.openSecureHttpConnection(remoteURL);
InputStream in = connection.getInputStream();
Credentials ts = new Credentials();
DataInputStream dis = new DataInputStream(in);
try {
ts.readFields(dis);
for(Token<?> token: ts.getAllTokens()) {
if (https) {
token.setKind(HsftpFileSystem.TOKEN_KIND);
} else {
token.setKind(HftpFileSystem.TOKEN_KIND);
}
SecurityUtil.setTokenService(token, nnAddr);
}
dis.close();
} catch (IOException ie) {
IOUtils.cleanup(LOG, dis);
}
return ts;
}
});
} catch (InterruptedException ie) {
return null;
}
}
/**
* Get the URI that we use for getting, renewing, and cancelling the
* delegation token. For KSSL with hftp that means we need to use
* https and the NN's https port.
*/
protected static String getRenewAddress(String protocol,
InetSocketAddress addr,
Configuration conf) {
if (SecurityUtil.useKsslAuth() && "http".equals(protocol)) {
protocol = "https";
int port =
conf.getInt(DFSConfigKeys.DFS_NAMENODE_HTTPS_PORT_KEY,
DFSConfigKeys.DFS_NAMENODE_HTTPS_PORT_DEFAULT);
addr = new InetSocketAddress(addr.getAddress(), port);
}
return DFSUtil.createUri(protocol, addr).toString();
}
/**
* Renew a Delegation Token.
* @param protocol The protocol to renew over (http or https)
* @param addr the address of the NameNode
* @param tok the token to renew
* @param conf the configuration
* @return the Date that the token will expire next.
* @throws IOException
*/
static public long renewDelegationToken(String protocol,
InetSocketAddress addr,
Token<DelegationTokenIdentifier> tok,
Configuration conf
) throws IOException {
final String renewAddress = getRenewAddress(protocol, addr, conf);
final StringBuilder buf = new StringBuilder(renewAddress);
final String service = tok.getService().toString();
buf.append(RenewDelegationTokenServlet.PATH_SPEC);
buf.append("?");
buf.append(RenewDelegationTokenServlet.TOKEN);
buf.append("=");
buf.append(tok.encodeToUrlString());
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
try {
return ugi.doAs(new PrivilegedExceptionAction<Long>(){
public Long run() throws Exception {
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();
if (LOG.isDebugEnabled()) {
LOG.debug("Renewed token for " + service + " via " +
renewAddress);
}
return result;
} catch (IOException ie) {
LOG.info("Error renewing token for " + renewAddress, 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;
}
}
});
} catch (InterruptedException ie) {
return 0;
}
}
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 protocol,
InetSocketAddress addr,
Token<DelegationTokenIdentifier> tok,
Configuration conf
) throws IOException {
final String renewAddress = getRenewAddress(protocol, addr, conf);
StringBuilder buf = new StringBuilder(renewAddress);
buf.append(CancelDelegationTokenServlet.PATH_SPEC);
buf.append("?");
buf.append(CancelDelegationTokenServlet.TOKEN);
buf.append("=");
buf.append(tok.encodeToUrlString());
BufferedReader in = null;
try {
final URL url = new URL(buf.toString());
if (LOG.isDebugEnabled()) {
LOG.debug("cancelling token at " + buf.toString());
}
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
ugi.doAs(new PrivilegedExceptionAction<Void>(){
public Void run() throws Exception {
HttpURLConnection connection =
(HttpURLConnection)SecurityUtil.openSecureHttpConnection(url);
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("Error cancelling token for " +
renewAddress + " response: " +
connection.getResponseMessage());
}
return null;
}
});
if (LOG.isDebugEnabled()) {
LOG.debug("Cancelled token for " + tok.getService() + " via " +
renewAddress);
}
} catch (IOException ie) {
LOG.warn("Error cancelling token for " + renewAddress, ie);
IOUtils.cleanup(LOG, in);
throw ie;
} catch (InterruptedException ie) {
// PASS
}
}
}