diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh index c8343b7369..5e0758f54b 100755 --- a/apiml-package/src/main/resources/bin/start.sh +++ b/apiml-package/src/main/resources/bin/start.sh @@ -334,6 +334,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \ -Djgroups.bind.address=${ZWE_components_caching_service_storage_infinispan_jgroups_host:-${ZWE_configs_storage_infinispan_jgroups_host:-${ZWE_haInstance_hostname:-localhost}}} \ -Djgroups.bind.port=${ZWE_components_caching_service_storage_infinispan_jgroups_port:-${ZWE_configs_storage_infinispan_jgroups_port:-7600}} \ -Djgroups.keyExchange.port=${ZWE_components_caching_service_storage_infinispan_jgroups_keyExchange_port:-${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7601}} \ + -Djgroups.keyExchange.socketTimeout=${ZWE_components_caching_service_storage_infinispan_jgroups_keyExchange_socketTimeout:-${ZWE_configs_storage_infinispan_jgroups_keyExchange_socketTimeout:-5000}} \ -Djgroups.tcp.diag.enabled=${ZWE_components_caching_service_storage_infinispan_jgroups_tcp_diag_enabled:-${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-false}} \ -Dloader.path=${APIML_LOADER_PATH} \ -Dlogging.charset.console=${ZOWE_CONSOLE_LOG_CHARSET} \ diff --git a/caching-service-package/src/main/resources/bin/start.sh b/caching-service-package/src/main/resources/bin/start.sh index 5e77d594df..ebab053c6b 100755 --- a/caching-service-package/src/main/resources/bin/start.sh +++ b/caching-service-package/src/main/resources/bin/start.sh @@ -137,6 +137,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CACHING_CODE} ${JAVA_BIN_DIR}java \ -Djgroups.bind.port=${ZWE_configs_storage_infinispan_jgroups_port:-7600} \ -Djgroups.keyExchange.port=${ZWE_configs_storage_infinispan_jgroups_keyExchange_port:-7601} \ -Djgroups.tcp.diag.enabled=${ZWE_configs_storage_infinispan_jgroups_tcp_diag_enabled:-false} \ + -Djgroups.keyExchange.socketTimeout=${ZWE_configs_storage_infinispan_jgroups_keyExchange_socketTimeout:-5000} \ -Dcaching.storage.infinispan.initialHosts=${ZWE_configs_storage_infinispan_initialHosts:-localhost[7600]} \ -Dserver.address=${ZWE_configs_zowe_network_server_listenAddresses_0:-${ZWE_zowe_network_server_listenAddresses_0:-"0.0.0.0"}} \ -Dserver.ssl.enabled=${ZWE_configs_server_ssl_enabled:-true} \ diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchange.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchange.java new file mode 100644 index 0000000000..2f8b0c4215 --- /dev/null +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchange.java @@ -0,0 +1,290 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.caching.service.infinispan; + +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; +import org.jgroups.Address; +import org.jgroups.protocols.SSL_KEY_EXCHANGE; +import org.jgroups.stack.IpAddress; + +import javax.net.ssl.*; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +public class ApimlSslKeyExchange extends SSL_KEY_EXCHANGE { + + private static final ThreadLocal> EXCEPTIONS = new ThreadLocal<>(); + + private static void addException(Exception e) { + List exceptionList = EXCEPTIONS.get(); + if (exceptionList == null) { + exceptionList = new ArrayList<>(); + } + exceptionList.add(e); + EXCEPTIONS.set(exceptionList); + } + + String toString(Throwable t) { + List stack = new ArrayList<>(); + Throwable previous; + do { + stack.add(t); + previous = t; + t = t.getCause(); + } while ((t != null) && (t != previous)); + return stack.stream().map(Throwable::toString).collect(Collectors.joining(": ")); + } + + void printError(String message, List exceptionList) { + log.error("{}: {}", message, exceptionList.stream().map(this::toString).collect(Collectors.joining(", "))); + } + + void printError(String message) { + List exceptionList = EXCEPTIONS.get(); + if (exceptionList != null) { + printError(message, exceptionList); + EXCEPTIONS.remove(); + } + } + + void decorate(Exception e) { + List exceptionList = EXCEPTIONS.get(); + if (exceptionList != null) { + Iterator iterator = exceptionList.iterator(); + if ((e.getCause() == null) || (e.getCause() == e)) { + e.initCause(iterator.next()); + } + while (iterator.hasNext()) { + e.addSuppressed(iterator.next()); + } + } + } + + protected SSLServerSocket createServerSocket() throws Exception { + try { + return super.createServerSocket(); + } catch (Exception e) { + decorate(e); + throw e; + } finally { + printError("Cannot create server socket"); + } + } + + @Override + protected SSLSocket createSocketTo(Address target) throws Exception { + try { + return super.createSocketTo(target); + } catch (Exception e) { + decorate(e); + throw e; + } finally { + printError("Cannot create socket to remote address"); + } + } + + @Override + protected SSLSocket createSocketTo(IpAddress dest, SSLSocketFactory sslSocketFactory) { + try { + return super.createSocketTo(dest, sslSocketFactory); + } catch (RuntimeException re) { + decorate(re); + throw re; + } finally { + printError("Cannot create socket to remote address"); + } + } + + private SSLContext update(SSLContext context) { + return new SSLContextWrapper( + new SSLContextSpiWrapper( + null, + new SSLSocketFactoryWrapper(context.getSocketFactory()), + new SSLServerSocketFactoryWrapper(context.getServerSocketFactory()) + ), + context + ); + } + + @Override + public void init() throws Exception { + synchronized (ApimlSslKeyExchange.class) { + boolean update = (client_ssl_ctx == null || server_ssl_ctx == null); + super.init(); + if (update) { + super.client_ssl_ctx = update(super.client_ssl_ctx); + super.server_ssl_ctx = update(super.server_ssl_ctx); + } + } + } + + @Override + public SSL_KEY_EXCHANGE setClientSSLContext(SSLContext client_ssl_ctx) { + return super.setClientSSLContext(update(client_ssl_ctx)); + } + + @Override + public SSL_KEY_EXCHANGE setServerSSLContext(SSLContext server_ssl_ctx) { + return super.setServerSSLContext(update(server_ssl_ctx)); + } + + @RequiredArgsConstructor + static class SSLSocketFactoryWrapper extends SSLSocketFactory { + + @Delegate + private final SSLSocketFactory original; + + @Override + public String[] getDefaultCipherSuites() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public String[] getSupportedCipherSuites() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + throw new IllegalStateException("Not implemented"); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + try { + return original.createSocket(host, port); + } catch (IOException | RuntimeException e) { + addException(e); + throw e; + } + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + throw new IllegalStateException("Not implemented"); + } + + } + + @RequiredArgsConstructor + static class SSLServerSocketFactoryWrapper extends SSLServerSocketFactory { + + @Delegate + private final SSLServerSocketFactory original; + + @Override + public String[] getDefaultCipherSuites() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public String[] getSupportedCipherSuites() { + throw new IllegalStateException("Not implemented"); + } + + @Override + public ServerSocket createServerSocket(int port) throws IOException { + throw new IllegalStateException("Not implemented"); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog) throws IOException { + throw new IllegalStateException("Not implemented"); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException { + try { + return original.createServerSocket(port, backlog, ifAddress); + } catch (IOException | RuntimeException e) { + addException(e); + throw e; + } + } + + } + + @RequiredArgsConstructor + static class SSLContextSpiWrapper extends SSLContextSpi { + + @Delegate + private final SSLContextSpi original; + private final SSLSocketFactory sslSocketFactory; + private final SSLServerSocketFactory sslServerSocketFactory; + + @Override + protected void engineInit(KeyManager[] km, TrustManager[] tm, SecureRandom sr) throws KeyManagementException { + throw new IllegalStateException("Not implemented"); + } + + @Override + protected SSLSocketFactory engineGetSocketFactory() { + return this.sslSocketFactory; + } + + @Override + protected SSLServerSocketFactory engineGetServerSocketFactory() { + return this.sslServerSocketFactory; + } + + @Override + protected SSLEngine engineCreateSSLEngine() { + throw new IllegalStateException("Not implemented"); + } + + @Override + protected SSLEngine engineCreateSSLEngine(String host, int port) { + throw new IllegalStateException("Not implemented"); + } + + @Override + protected SSLSessionContext engineGetServerSessionContext() { + throw new IllegalStateException("Not implemented"); + } + + @Override + protected SSLSessionContext engineGetClientSessionContext() { + throw new IllegalStateException("Not implemented"); + } + + } + + static class SSLContextWrapper extends SSLContext { + + SSLContextWrapper(SSLContextSpi contextSpi, SSLContext original) { + super(contextSpi, original.getProvider(), original.getProtocol()); + } + + } + +} diff --git a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java index f95974695f..dc7174c396 100644 --- a/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java +++ b/caching-service/src/main/java/org/zowe/apiml/caching/service/infinispan/config/InfinispanConfig.java @@ -89,6 +89,9 @@ public class InfinispanConfig implements InitializingBean { @Value("${jgroups.bind.address}") private String address; + @Value("${jgroups.keyExchange.socketTimeout:5000}") + private String keyExchangeSocketTimeout; + @Value("${jgroups.keyExchange.port:7601}") private String keyExchangePort; @@ -98,9 +101,6 @@ public class InfinispanConfig implements InitializingBean { @Value("${attlsEnabledOnInfinispanTest:${server.attlsServer.enabled:false}}") private boolean isServerAttlsEnabled; - @Value("${apiml.service.hostname:localhost}") - private String hostname; - @Value("${caching.storage.infinispan.distributedSyncTimeoutSecs:360}") private int distributedSyncTimeout; @@ -181,6 +181,7 @@ synchronized LazyCacheManager cacheManager(ResourceLoader resourceLoader, Applic System.setProperty("jgroups.tcpping.initial_hosts", initialHosts); System.setProperty("jgroups.bind.port", port); System.setProperty("jgroups.bind.address", address); + System.setProperty("jgroups.keyExchange.socketTimeout", keyExchangeSocketTimeout); System.setProperty("jgroups.keyExchange.port", keyExchangePort); System.setProperty("jgroups.tcp.diag.enabled", String.valueOf(Boolean.parseBoolean(tcpDiagEnabled))); diff --git a/caching-service/src/main/resources/infinispan.xml b/caching-service/src/main/resources/infinispan.xml index b19355f3b9..77ea9acb10 100644 --- a/caching-service/src/main/resources/infinispan.xml +++ b/caching-service/src/main/resources/infinispan.xml @@ -33,7 +33,8 @@ timeout_check_interval="1000" /> - + - + diff --git a/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchangeTest.java b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchangeTest.java new file mode 100644 index 0000000000..425a06b053 --- /dev/null +++ b/caching-service/src/test/java/org/zowe/apiml/caching/service/infinispan/ApimlSslKeyExchangeTest.java @@ -0,0 +1,159 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.caching.service.infinispan; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.Appender; +import com.sun.net.httpserver.HttpServer; +import org.jgroups.Event; +import org.jgroups.stack.IpAddress; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSocketFactory; +import java.net.BindException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ApimlSslKeyExchangeTest { + + private static final String[] acceptedInvalidAddressMessages = { + /* Windows */"Cannot assign requested address: connect", + /* Linux (Github Actions */ "Connection refused", + /* MacOS */ "Can't assign requested address" + }; + + private static final IpAddress INVALID_ADDRESS = new IpAddress(); + + @Mock + private Appender mockedAppender; + + @Captor + private ArgumentCaptor loggingEventCaptor; + + private ApimlSslKeyExchange apimlSslKeyExchange; + + private Logger logger; + + @BeforeEach + void setUp() throws Exception { + logger = (Logger) LoggerFactory.getLogger(ApimlSslKeyExchange.class); + logger.getLoggerContext().resetTurboFilterList(); // Turbo filters remove duplicities + logger.addAppender(mockedAppender); + logger.setLevel(Level.ERROR); + + apimlSslKeyExchange = createApimlSslKeyExchange(); + } + + private ApimlSslKeyExchange createApimlSslKeyExchange() throws Exception { + ApimlSslKeyExchange apimlSslKeyExchange = new ApimlSslKeyExchange() { + @Override + public Object down(Event evt) { + return evt.getArg(); + } + }; + + apimlSslKeyExchange.setPortRange(0); + apimlSslKeyExchange.setKeystoreName("../keystore/localhost/localhost.keystore.p12"); + apimlSslKeyExchange.setKeystorePassword("password"); + apimlSslKeyExchange.setKeystoreType("PKCS12"); + apimlSslKeyExchange.setTruststoreName("../keystore/localhost/localhost.truststore.p12"); + apimlSslKeyExchange.setTruststorePassword("password"); + apimlSslKeyExchange.setTruststoreType("PKCS12"); + + apimlSslKeyExchange.setDownProtocol(apimlSslKeyExchange); + + apimlSslKeyExchange.init(); + + return apimlSslKeyExchange; + } + + private String getLogMessage() { + verify(mockedAppender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + List logMessages = loggingEventCaptor.getAllValues(); + assertEquals(1, logMessages.size()); + return logMessages.get(0).getFormattedMessage(); + } + + + @Test + void givenOccupiedPort_whenBecomeKeyserver_thenLogTheError() throws Exception { + HttpServer occupiedPort = HttpServer.create(new InetSocketAddress(apimlSslKeyExchange.getPort()), 0); + try { + occupiedPort.start(); + + IllegalStateException e = assertThrows(IllegalStateException.class, apimlSslKeyExchange::createServerSocket); + assertNotNull(e.getCause()); + assertTrue(e.getCause().getMessage().contains("Address already in use"), "Unexpected cause message: " + e.getCause().getMessage()); + + String logMessage = getLogMessage(); + assertTrue(logMessage.contains("Cannot create server socket: "), "Unexpected message: " + logMessage); + assertTrue(logMessage.contains("Address already in use"), "Unexpected message: " + logMessage); + assertTrue(logMessage.contains("BindException"), "Unexpected message: " + logMessage); + } finally { + occupiedPort.stop(0); + } + } + + private boolean containsAny(String message, String...expected) { + for (String s : expected) { + if (message.contains(s)) { + return true; + } + } + return false; + } + + @Test + void givenInvalidTarget_whenCreateSocketTo_thenLogTheError() { + IllegalStateException e = assertThrows(IllegalStateException.class, () -> apimlSslKeyExchange.createSocketTo(INVALID_ADDRESS)); + + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof ConnectException || e.getCause() instanceof BindException, "Unexpected exception: " + e.getCause().getClass()); + assertTrue(containsAny(e.getCause().getMessage(), acceptedInvalidAddressMessages), "Unexpected cause message: " + e.getCause().getMessage()); + + String logMessage = getLogMessage(); + assertTrue(logMessage.contains("Cannot create socket to remote address"), "Unexpected message: " + logMessage); + assertTrue(containsAny(logMessage, acceptedInvalidAddressMessages), "Unexpected message: " + logMessage); + assertTrue(containsAny(logMessage, "BindException:","ConnectException:", "java.net.NoRouteToHostException"), "Unexpected message: " + logMessage); + } + + @Test + void givenInvalidTargetWithSslFactory_whenCreateSocketTo_thenLogTheError() { + SSLSocketFactory sslSocketFactory = apimlSslKeyExchange.getClientSSLContext().getSocketFactory(); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> apimlSslKeyExchange.createSocketTo(INVALID_ADDRESS, sslSocketFactory)); + + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof ConnectException || e.getCause() instanceof BindException, "Unexpected exception: " + e.getCause().getClass()); + assertTrue(containsAny(e.getCause().getMessage(), acceptedInvalidAddressMessages), "Unexpected message: " + e.getCause().getMessage()); + + String logMessage = getLogMessage(); + assertTrue(logMessage.contains("Cannot create socket to remote address"), "Unexpected message: " + logMessage); + assertTrue(containsAny(logMessage, acceptedInvalidAddressMessages), "Unexpected message: " + logMessage); + assertTrue(containsAny(logMessage, "BindException:", "ConnectException:", "java.net.NoRouteToHostException"), "Unexpected message: " + logMessage); + } + +}