/** * 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.ambari.server; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.ambari.server.configuration.Configuration; import org.apache.commons.lang.StringUtils; import org.apache.directory.kerberos.client.KdcConfig; import org.apache.directory.kerberos.client.KdcConnection; import org.apache.directory.shared.kerberos.KerberosMessageType; import org.apache.directory.shared.kerberos.exceptions.ErrorType; import org.apache.directory.shared.kerberos.exceptions.KerberosException; import org.apache.directory.shared.kerberos.messages.KrbError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Singleton; /** * Utility class which checks connection to Kerberos Server. * <p> * It has two potential clients. * <ul> * <li>Ambari Agent: * Uses it to make sure host can talk to specified KDC Server * </li> * <p/> * <li>Ambari Server: * Uses it for connection check, like agent, and also validates * the credentials provided on Server side. * </li> * </ul> * </p> */ @Singleton public class KdcServerConnectionVerification { private static Logger LOG = LoggerFactory.getLogger(KdcServerConnectionVerification.class); private Configuration config; /** * The connection timeout in seconds. */ private int connectionTimeout = 10; @Inject public KdcServerConnectionVerification(Configuration config) { this.config = config; } /** * Given server IP or hostname, checks if server is reachable i.e. * we can make a socket connection to it. Hostname may contain port * number separated by a colon. * * @param kdcHost KDC server IP or hostname (with optional port number) * @return true, if server is accepting connection given port; false otherwise. */ public boolean isKdcReachable(String kdcHost) { try { if (kdcHost == null || kdcHost.isEmpty()) { throw new IllegalArgumentException("Invalid hostname for KDC server"); } String[] kdcDetails = kdcHost.split(":"); if (kdcDetails.length == 1) { return isKdcReachable(kdcDetails[0], parsePort(config.getDefaultKdcPort())); } else { return isKdcReachable(kdcDetails[0], parsePort(kdcDetails[1])); } } catch (Exception e) { LOG.error("Exception while checking KDC reachability: " + e); return false; } } /** * Given a host and port, checks if server is reachable meaning that we * can communicate with it. First we attempt to connect via TCP and if * that is unsuccessful, attempt via UDP. It is important to understand that * we are not validating credentials, only attempting to communicate with server * process for the give host and port. * * @param server KDC server IP or hostname * @param port KDC port * @return true, if server is accepting connection given port; false otherwise. */ public boolean isKdcReachable(String server, int port) { boolean success = isKdcReachable(server, port, ConnectionProtocol.TCP) || isKdcReachable(server, port, ConnectionProtocol.UDP); if (!success) { LOG.error("Failed to connect to the KDC at {}:{} using either TCP or UDP", server, port); } return success; } /** * Attempt to communicate with KDC server over a specified communication protocol (TCP or UDP). * * @param server KDC hostname or IP address * @param port KDC server port * @param connectionProtocol the type of connection to use * @return true if communication is successful; false otherwise */ public boolean isKdcReachable(final String server, final int port, final ConnectionProtocol connectionProtocol) { int timeoutMillis = connectionTimeout * 1000; final KdcConfig config = KdcConfig.getDefaultConfig(); config.setHostName(server); config.setKdcPort(port); config.setUseUdp(ConnectionProtocol.UDP == connectionProtocol); config.setTimeout(timeoutMillis); FutureTask<Boolean> future = new FutureTask<>(new Callable<Boolean>() { @Override public Boolean call() { Boolean success; try { KdcConnection connection = getKdcConnection(config); // we are only testing whether we can communicate with server and not // validating credentials connection.getTgt("noUser@noRealm", "noPassword"); LOG.info(String.format("Encountered no Exceptions while testing connectivity to the KDC:\n" + "**** Host: %s:%d (%s)", server, port, connectionProtocol.name())); success = true; } catch (KerberosException e) { KrbError error = e.getError(); ErrorType errorCode = error.getErrorCode(); String errorCodeMessage; int errorCodeCode; if (errorCode != null) { errorCodeMessage = errorCode.getMessage(); errorCodeCode = errorCode.getValue(); } else { errorCodeMessage = "<Not Specified>"; errorCodeCode = -1; } // unfortunately, need to look at msg as error 60 is a generic error code //todo: evaluate other error codes to provide better information //todo: as there may be other error codes where we should return false success = !(errorCodeCode == ErrorType.KRB_ERR_GENERIC.getValue() && errorCodeMessage.contains("TimeOut")); if (!success || LOG.isDebugEnabled()) { KerberosMessageType messageType = error.getMessageType(); String messageTypeMessage; int messageTypeCode; if (messageType != null) { messageTypeMessage = messageType.getMessage(); messageTypeCode = messageType.getValue(); } else { messageTypeMessage = "<Not Specified>"; messageTypeCode = -1; } String message = String.format("Received KerberosException while testing connectivity to the KDC: %s\n" + "**** Host: %s:%d (%s)\n" + "**** Error: %s\n" + "**** Code: %d (%s)\n" + "**** Message: %d (%s)", e.getLocalizedMessage(), server, port, connectionProtocol.name(), error.getEText(), errorCodeCode, errorCodeMessage, messageTypeCode, messageTypeMessage); if (LOG.isDebugEnabled()) { LOG.info(message, e); } else { LOG.info(message); } } } catch (Throwable e) { LOG.info(String.format("Received Exception while testing connectivity to the KDC: %s\n**** Host: %s:%d (%s)", e.getLocalizedMessage(), server, port, connectionProtocol.name()), e); // some bad unexpected thing occurred throw new RuntimeException(e); } return success; } }); new Thread(future, "ambari-kdc-verify").start(); Boolean result; try { // timeout after specified timeout result = future.get(timeoutMillis, TimeUnit.MILLISECONDS); if (result) { LOG.info(String.format("Successfully connected to the KDC server at %s:%d over %s", server, port, connectionProtocol.name())); } else { LOG.warn(String.format("Failed to connect to the KDC server at %s:%d over %s", server, port, connectionProtocol.name())); } } catch (InterruptedException e) { String message = String.format("Interrupted while trying to communicate with KDC server at %s:%d over %s", server, port, connectionProtocol.name()); if (LOG.isDebugEnabled()) { LOG.warn(message, e); } else { LOG.warn(message); } result = false; future.cancel(true); } catch (ExecutionException e) { String message = String.format("An unexpected exception occurred while attempting to communicate with the KDC server at %s:%d over %s", server, port, connectionProtocol.name()); if (LOG.isDebugEnabled()) { LOG.warn(message, e); } else { LOG.warn(message); } result = false; } catch (TimeoutException e) { String message = String.format("Timeout occurred while attempting to to communicate with KDC server at %s:%d over %s", server, port, connectionProtocol.name()); if (LOG.isDebugEnabled()) { LOG.warn(message, e); } else { LOG.warn(message); } result = false; future.cancel(true); } return result; } /** * Get a KDC UDP connection for the given configuration. * This has been extracted into it's own method primarily * for unit testing purposes. * * @param config KDC connection configuration * @return new KDC connection */ protected KdcConnection getKdcConnection(KdcConfig config) { return new KdcConnection(config); } /** * Set the connection timeout. * This is the amount of time that we will attempt to read data from connection. * * @param timeoutSeconds timeout in seconds */ public void setConnectionTimeout(int timeoutSeconds) { connectionTimeout = (timeoutSeconds < 1) ? 1 : timeoutSeconds; } /** * Get the timeout value. * * @return the connection timeout value in seconds */ public int getConnectionTimeout() { return connectionTimeout; } /** * Parses port number from given string. * * @param port port number string * @return parsed port number * @throws NumberFormatException if given string cannot be parsed * @throws IllegalArgumentException if given string is null or empty */ protected int parsePort(String port) { if (StringUtils.isEmpty(port)) { throw new IllegalArgumentException("Port number must be non-empty, non-null positive integer"); } return Integer.parseInt(port); } /** * A connection protocol to use to for connecting to the KDC */ public enum ConnectionProtocol { TCP, UDP } }