blob: 82c9e70442242d58bd8d78742e0c015f538fd8e5 [file] [log] [blame]
/*
* 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.geode.management.internal.cli.commands;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_PREFIX;
import static org.apache.geode.distributed.ConfigurationProperties.HTTP_SERVICE_SSL_PREFIX;
import static org.apache.geode.distributed.ConfigurationProperties.JMX_MANAGER_SSL_PREFIX;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Properties;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import org.apache.commons.lang3.StringUtils;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.apache.geode.annotations.Immutable;
import org.apache.geode.internal.admin.SSLConfig;
import org.apache.geode.internal.net.SSLConfigurationFactory;
import org.apache.geode.internal.security.SecurableCommunicationChannel;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.cli.ConverterHint;
import org.apache.geode.management.internal.JmxManagerLocatorRequest;
import org.apache.geode.management.internal.JmxManagerLocatorResponse;
import org.apache.geode.management.internal.SSLUtil;
import org.apache.geode.management.internal.cli.CliUtil;
import org.apache.geode.management.internal.cli.LogWrapper;
import org.apache.geode.management.internal.cli.converters.ConnectionEndpointConverter;
import org.apache.geode.management.internal.cli.domain.ConnectToLocatorResult;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
import org.apache.geode.management.internal.cli.result.model.InfoResultModel;
import org.apache.geode.management.internal.cli.result.model.ResultModel;
import org.apache.geode.management.internal.cli.shell.Gfsh;
import org.apache.geode.management.internal.cli.shell.JmxOperationInvoker;
import org.apache.geode.management.internal.cli.shell.OperationInvoker;
import org.apache.geode.management.internal.cli.util.ConnectionEndpoint;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.apache.geode.management.internal.web.shell.HttpOperationInvoker;
import org.apache.geode.security.AuthenticationFailedException;
public class ConnectCommand extends OfflineGfshCommand {
// millis that connect --locator will wait for a response from the locator.
static final int CONNECT_LOCATOR_TIMEOUT_MS = 60000; // see bug 45971
private static final int VERSION_MAJOR = 0;
private static final int VERSION_MINOR = 1;
@Immutable
private static final UserInputProperty[] USER_INPUT_PROPERTIES =
{UserInputProperty.KEYSTORE, UserInputProperty.KEYSTORE_PASSWORD,
UserInputProperty.KEYSTORE_TYPE, UserInputProperty.TRUSTSTORE,
UserInputProperty.TRUSTSTORE_PASSWORD, UserInputProperty.TRUSTSTORE_TYPE,
UserInputProperty.CIPHERS, UserInputProperty.PROTOCOL, UserInputProperty.COMPONENT};
@CliCommand(value = {CliStrings.CONNECT}, help = CliStrings.CONNECT__HELP)
@CliMetaData(shellOnly = true, relatedTopic = {CliStrings.TOPIC_GFSH, CliStrings.TOPIC_GEODE_JMX,
CliStrings.TOPIC_GEODE_MANAGER})
public ResultModel connect(
@CliOption(key = {CliStrings.CONNECT__LOCATOR},
unspecifiedDefaultValue = ConnectionEndpointConverter.DEFAULT_LOCATOR_ENDPOINTS,
optionContext = ConnectionEndpoint.LOCATOR_OPTION_CONTEXT,
help = CliStrings.CONNECT__LOCATOR__HELP) ConnectionEndpoint locatorEndPoint,
@CliOption(key = {CliStrings.CONNECT__JMX_MANAGER},
optionContext = ConnectionEndpoint.JMXMANAGER_OPTION_CONTEXT,
help = CliStrings.CONNECT__JMX_MANAGER__HELP) ConnectionEndpoint jmxManagerEndPoint,
@CliOption(key = {CliStrings.CONNECT__USE_HTTP}, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false",
help = CliStrings.CONNECT__USE_HTTP__HELP) boolean useHttp,
@CliOption(key = {CliStrings.CONNECT__URL}, help = CliStrings.CONNECT__URL__HELP) String url,
@CliOption(key = {CliStrings.CONNECT__USERNAME},
help = CliStrings.CONNECT__USERNAME__HELP) String userName,
@CliOption(key = {CliStrings.CONNECT__PASSWORD},
help = CliStrings.CONNECT__PASSWORD__HELP) String password,
@CliOption(key = {CliStrings.CONNECT__KEY_STORE},
help = CliStrings.CONNECT__KEY_STORE__HELP) String keystore,
@CliOption(key = {CliStrings.CONNECT__KEY_STORE_PASSWORD},
help = CliStrings.CONNECT__KEY_STORE_PASSWORD__HELP) String keystorePassword,
@CliOption(key = {CliStrings.CONNECT__TRUST_STORE},
help = CliStrings.CONNECT__TRUST_STORE__HELP) String truststore,
@CliOption(key = {CliStrings.CONNECT__TRUST_STORE_PASSWORD},
help = CliStrings.CONNECT__TRUST_STORE_PASSWORD__HELP) String truststorePassword,
@CliOption(key = {CliStrings.CONNECT__SSL_CIPHERS},
help = CliStrings.CONNECT__SSL_CIPHERS__HELP) String sslCiphers,
@CliOption(key = {CliStrings.CONNECT__SSL_PROTOCOLS},
help = CliStrings.CONNECT__SSL_PROTOCOLS__HELP) String sslProtocols,
@CliOption(key = CliStrings.CONNECT__SECURITY_PROPERTIES, optionContext = ConverterHint.FILE,
help = CliStrings.CONNECT__SECURITY_PROPERTIES__HELP) final File gfSecurityPropertiesFile,
@CliOption(key = {CliStrings.CONNECT__USE_SSL}, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false",
help = CliStrings.CONNECT__USE_SSL__HELP) boolean useSsl,
@CliOption(key = {"skip-ssl-validation"}, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false",
help = "When connecting via HTTP, connects using 1-way SSL validation rather than 2-way SSL validation.") boolean skipSslValidation) {
ResultModel result = new ResultModel();
Gfsh gfsh = getGfsh();
// bail out if gfsh is already connected.
if (gfsh != null && gfsh.isConnectedAndReady()) {
return ResultModel
.createInfo("Already connected to: " + getGfsh().getOperationInvoker().toString());
}
if (StringUtils.startsWith(url, "https")) {
useSsl = true;
}
// ssl options are passed in in the order defined in USER_INPUT_PROPERTIES, note the two types
// are null, because we don't have connect command options for them yet
Properties gfProperties = resolveSslProperties(gfsh, useSsl, null, gfSecurityPropertiesFile,
keystore, keystorePassword, null, truststore, truststorePassword, null, sslCiphers,
sslProtocols, null);
if (containsSSLConfig(gfProperties) || containsLegacySSLConfig(gfProperties)) {
useSsl = true;
}
// if username is specified in the option but password is not, prompt for the password
// note if gfProperties has username but no password, we would not prompt for password yet,
// because we may not need username/password combination to connect.
if (userName != null) {
gfProperties.setProperty(ResourceConstants.USER_NAME, userName);
if (password == null) {
password = UserInputProperty.PASSWORD.promptForAcceptableValue(gfsh);
}
gfProperties.setProperty(UserInputProperty.PASSWORD.getKey(), password);
}
if (StringUtils.isNotEmpty(url)) {
result = httpConnect(gfProperties, url, skipSslValidation);
} else {
result = jmxConnect(gfProperties, useSsl, jmxManagerEndPoint, locatorEndPoint, false);
}
OperationInvoker invoker = gfsh.getOperationInvoker();
if (invoker == null || !invoker.isConnected()) {
return result;
}
String gfshVersion = gfsh.getVersion();
String remoteVersion = null;
try {
String gfshGeodeSerializationVersion = gfsh.getGeodeSerializationVersion();
String remoteGeodeSerializationVersion = invoker.getRemoteGeodeSerializationVersion();
if (hasSameMajorMinor(gfshGeodeSerializationVersion, remoteGeodeSerializationVersion)) {
return result;
}
} catch (Exception e) {
// we failed to get the remote geode serialization version; get remote product version for
// error message
try {
remoteVersion = invoker.getRemoteVersion();
} catch (Exception ex) {
gfsh.logInfo("failed to get the the remote version.", ex);
}
}
// will reach here only when remoteVersion is not available or does not match
invoker.stop();
if (remoteVersion == null) {
return ResultModel.createError(
String.format("Cannot use a %s gfsh client to connect to this cluster.", gfshVersion));
} else {
return ResultModel.createError(String.format(
"Cannot use a %s gfsh client to connect to a %s cluster.", gfshVersion, remoteVersion));
}
}
private static boolean hasSameMajorMinor(String gfshVersion, String remoteVersion) {
return versionComponent(remoteVersion, VERSION_MAJOR)
.equalsIgnoreCase(versionComponent(gfshVersion, VERSION_MAJOR))
&& versionComponent(remoteVersion, VERSION_MINOR)
.equalsIgnoreCase(versionComponent(gfshVersion, VERSION_MINOR));
}
private static String versionComponent(String version, int component) {
String[] versionComponents = StringUtils.split(version, '.');
return versionComponents.length >= component + 1 ? versionComponents[component] : "";
}
/**
* @param useSsl if true, and no files/options passed, we would still insist on prompting for ssl
* config (considered only when the last three parameters are null)
* @param gfPropertiesFile gemfire properties file, can be null
* @param gfSecurityPropertiesFile gemfire security properties file, can be null
* @param sslOptionValues an array of 9 in this order, as defined in USER_INPUT_PROPERTIES
* @return the properties
*/
Properties resolveSslProperties(Gfsh gfsh, boolean useSsl, File gfPropertiesFile,
File gfSecurityPropertiesFile, String... sslOptionValues) {
// first trying to load the sslProperties from the file
Properties gfProperties = loadProperties(gfPropertiesFile, gfSecurityPropertiesFile);
// if the security file is a legacy ssl security file, then the rest of the command options, if
// any, are ignored. Because we are not trying to add/replace the legacy ssl values using the
// command line values. all command line ssl values updates the ssl-* options.
if (containsLegacySSLConfig(gfProperties)) {
return gfProperties;
}
// if nothing indicates we should prompt for missing ssl config info, return immediately
if (!(useSsl || containsSSLConfig(gfProperties) || isSslImpliedBySslOptions(sslOptionValues))) {
return gfProperties;
}
// if use ssl is implied by any of the options, then command option will add to/update the
// properties loaded from file. If the ssl config is not specified anywhere, prompt user for it.
for (int i = 0; i < USER_INPUT_PROPERTIES.length; i++) {
UserInputProperty userInputProperty = USER_INPUT_PROPERTIES[i];
String sslOptionValue = null;
if (sslOptionValues != null && sslOptionValues.length > i) {
sslOptionValue = sslOptionValues[i];
}
String sslConfigValue = gfProperties.getProperty(userInputProperty.getKey());
// if this option is specified, always use this value
if (sslOptionValue != null) {
gfProperties.setProperty(userInputProperty.getKey(), sslOptionValue);
}
// if option is not specified and not present in the original properties, prompt for it
else if (sslConfigValue == null) {
gfProperties.setProperty(userInputProperty.getKey(),
userInputProperty.promptForAcceptableValue(gfsh));
}
}
return gfProperties;
}
boolean isSslImpliedBySslOptions(String... sslOptions) {
return sslOptions != null && Arrays.stream(sslOptions).anyMatch(Objects::nonNull);
}
Properties loadProperties(File... files) {
Properties properties = new Properties();
if (files == null) {
return properties;
}
for (File file : files) {
if (file != null) {
properties.putAll(loadPropertiesFromFile(file));
}
}
return properties;
}
static boolean containsLegacySSLConfig(Properties properties) {
return properties.stringPropertyNames().stream()
.anyMatch(key -> key.startsWith(CLUSTER_SSL_PREFIX)
|| key.startsWith(JMX_MANAGER_SSL_PREFIX) || key.startsWith(HTTP_SERVICE_SSL_PREFIX));
}
private static boolean containsSSLConfig(Properties properties) {
return properties.stringPropertyNames().stream().anyMatch(key -> key.startsWith("ssl-"));
}
ResultModel httpConnect(Properties gfProperties, String url, boolean skipSslVerification) {
Gfsh gfsh = getGfsh();
try {
SSLConfig sslConfig = SSLConfigurationFactory.getSSLConfigForComponent(gfProperties,
SecurableCommunicationChannel.WEB);
if (sslConfig.isEnabled()) {
configureHttpsURLConnection(sslConfig, skipSslVerification);
if (url.startsWith("http:")) {
url = url.replace("http:", "https:");
}
}
// authentication check will be triggered inside the constructor
HttpOperationInvoker operationInvoker = new HttpOperationInvoker(gfsh, url, gfProperties);
gfsh.setOperationInvoker(operationInvoker);
LogWrapper.getInstance().info(
CliStrings.format(CliStrings.CONNECT__MSG__SUCCESS, operationInvoker.toString()));
return ResultModel.createInfo(
CliStrings.format(CliStrings.CONNECT__MSG__SUCCESS, operationInvoker.toString()));
} catch (SecurityException | AuthenticationFailedException e) {
// if it's security exception, and we already sent in username and password, still returns the
// connection error
if (gfProperties.containsKey(ResourceConstants.USER_NAME)) {
return handleException(e);
}
// otherwise, prompt for username and password and retry the connection
gfProperties.setProperty(UserInputProperty.USERNAME.getKey(),
UserInputProperty.USERNAME.promptForAcceptableValue(gfsh));
gfProperties.setProperty(UserInputProperty.PASSWORD.getKey(),
UserInputProperty.PASSWORD.promptForAcceptableValue(gfsh));
return httpConnect(gfProperties, url, skipSslVerification);
} catch (Exception e) {
// all other exceptions, just logs it and returns a connection error
return handleException(e);
}
}
ResultModel jmxConnect(Properties gfProperties, boolean useSsl,
ConnectionEndpoint memberRmiHostPort,
ConnectionEndpoint locatorTcpHostPort, boolean retry) {
ConnectionEndpoint jmxHostPortToConnect = null;
Gfsh gfsh = getGfsh();
try {
// trying to find the rmi host and port, if rmi host port exists, use that, otherwise, use
// locator to find the rmi host port
if (memberRmiHostPort != null) {
jmxHostPortToConnect = memberRmiHostPort;
} else {
if (useSsl) {
gfsh.logToFile(
CliStrings.CONNECT__USE_SSL + " is set to true. Connecting to Locator via SSL.",
null);
}
Gfsh.println(CliStrings.format(CliStrings.CONNECT__MSG__CONNECTING_TO_LOCATOR_AT_0,
new Object[] {locatorTcpHostPort.toString(false)}));
ConnectToLocatorResult connectToLocatorResult =
connectToLocator(locatorTcpHostPort.getHost(), locatorTcpHostPort.getPort(),
CONNECT_LOCATOR_TIMEOUT_MS, gfProperties);
jmxHostPortToConnect = connectToLocatorResult.getMemberEndpoint();
// when locator is configured to use SSL (ssl-enabled=true) but manager is not
// (jmx-manager-ssl=false)
if (useSsl && !connectToLocatorResult.isJmxManagerSslEnabled()) {
gfsh.logInfo(
CliStrings.CONNECT__USE_SSL
+ " is set to true. But JMX Manager doesn't support SSL, connecting without SSL.",
null);
useSsl = false;
}
}
if (useSsl) {
gfsh.logToFile("Connecting to manager via SSL.", null);
}
// print out the connecting endpoint
if (!retry) {
Gfsh.println(CliStrings.format(CliStrings.CONNECT__MSG__CONNECTING_TO_MANAGER_AT_0,
new Object[] {jmxHostPortToConnect.toString(false)}));
}
ResultModel result = new ResultModel();
InfoResultModel infoResultModel = result.addInfo();
JmxOperationInvoker operationInvoker = new JmxOperationInvoker(jmxHostPortToConnect.getHost(),
jmxHostPortToConnect.getPort(), gfProperties);
gfsh.setOperationInvoker(operationInvoker);
infoResultModel.addLine(CliStrings.format(CliStrings.CONNECT__MSG__SUCCESS,
jmxHostPortToConnect.toString(false)));
LogWrapper.getInstance().info(CliStrings.format(CliStrings.CONNECT__MSG__SUCCESS,
jmxHostPortToConnect.toString(false)));
return result;
} catch (SecurityException | AuthenticationFailedException e) {
// if it's security exception, and we already sent in username and password, still returns the
// connection error
if (gfProperties.containsKey(ResourceConstants.USER_NAME)) {
return handleException(e, jmxHostPortToConnect);
}
// otherwise, prompt for username and password and retry the connection
gfProperties.setProperty(UserInputProperty.USERNAME.getKey(),
UserInputProperty.USERNAME.promptForAcceptableValue(gfsh));
gfProperties.setProperty(UserInputProperty.PASSWORD.getKey(),
UserInputProperty.PASSWORD.promptForAcceptableValue(gfsh));
return jmxConnect(gfProperties, useSsl, jmxHostPortToConnect, null, true);
} catch (UnknownHostException e) {
return handleException(e,
"JMX manager can't be reached. Hostname or IP address could not be found.");
} catch (Exception e) {
// all other exceptions, just logs it and returns a connection error
return handleException(e, jmxHostPortToConnect);
}
}
public static ConnectToLocatorResult connectToLocator(String host, int port, int timeout,
Properties props) throws IOException, ClassNotFoundException {
JmxManagerLocatorResponse locatorResponse =
JmxManagerLocatorRequest.send(host, port, timeout, props);
if (StringUtils.isBlank(locatorResponse.getHost()) || locatorResponse.getPort() == 0) {
Throwable locatorResponseException = locatorResponse.getException();
String exceptionMessage = CliStrings.CONNECT__MSG__LOCATOR_COULD_NOT_FIND_MANAGER;
if (locatorResponseException != null) {
String locatorResponseExceptionMessage = locatorResponseException.getMessage();
locatorResponseExceptionMessage = (StringUtils.isNotBlank(locatorResponseExceptionMessage)
? locatorResponseExceptionMessage : locatorResponseException.toString());
exceptionMessage = "Exception caused JMX Manager startup to fail because: '"
.concat(locatorResponseExceptionMessage).concat("'");
}
throw new IllegalStateException(exceptionMessage, locatorResponseException);
}
ConnectionEndpoint memberEndpoint =
new ConnectionEndpoint(locatorResponse.getHost(), locatorResponse.getPort());
String resultMessage = CliStrings.format(CliStrings.CONNECT__MSG__CONNECTING_TO_MANAGER_AT_0,
memberEndpoint.toString(false));
return new ConnectToLocatorResult(memberEndpoint, resultMessage,
locatorResponse.isJmxManagerSslEnabled());
}
private void configureHttpsURLConnection(SSLConfig sslConfig, boolean skipSslVerification) {
SSLContext ssl = SSLUtil.createAndConfigureSSLContext(sslConfig, skipSslVerification);
if (skipSslVerification) {
HttpsURLConnection.setDefaultHostnameVerifier((String s, SSLSession sslSession) -> true);
}
HttpsURLConnection.setDefaultSSLSocketFactory(ssl.getSocketFactory());
}
private ResultModel handleException(Exception e) {
return handleException(e, e.getMessage());
}
private ResultModel handleException(Exception e, String errorMessage) {
LogWrapper.getInstance().severe(errorMessage, e);
return ResultModel.createError(errorMessage);
}
private ResultModel handleException(Exception e, ConnectionEndpoint hostPortToConnect) {
if (hostPortToConnect == null) {
return handleException(e);
}
return handleException(e, CliStrings.format(CliStrings.CONNECT__MSG__ERROR,
hostPortToConnect.toString(false), e.getMessage()));
}
private static Properties loadPropertiesFromFile(File propertyFile) {
try {
return loadPropertiesFromUrl(propertyFile.toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(
CliStrings.format("Failed to load configuration properties from pathname (%1$s)!",
propertyFile.getAbsolutePath()),
e);
}
}
private static Properties loadPropertiesFromUrl(URL url) {
Properties properties = new Properties();
if (url == null) {
return properties;
}
try (InputStream inputStream = url.openStream()) {
properties.load(inputStream);
} catch (IOException io) {
throw new RuntimeException(
CliStrings.format(CliStrings.CONNECT__MSG__COULD_NOT_READ_CONFIG_FROM_0,
CliUtil.decodeWithDefaultCharSet(url.getPath())),
io);
}
return properties;
}
}