changeset 203:de32338d6dcd

[TLS] Make the connector (optionally) encrypted. Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/024126.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Mon, 17 Jul 2017 11:45:37 +0200
parents c0140877e78b
children 6e9a0f606b38
files common/core/src/main/java/com/redhat/thermostat/gateway/common/core/config/GlobalConfiguration.java distribution/src/etc/global-config.properties server/src/main/java/com/redhat/thermostat/gateway/server/CoreServerBuilder.java server/src/main/java/com/redhat/thermostat/gateway/server/Start.java server/src/test/java/com/redhat/thermostat/gateway/server/CoreServerBuilderTest.java server/src/test/resources/test_gw_home/test_me.jks tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java
diffstat 7 files changed, 208 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/config/GlobalConfiguration.java	Mon Jul 17 11:37:33 2017 +0200
+++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/config/GlobalConfiguration.java	Mon Jul 17 11:45:37 2017 +0200
@@ -80,7 +80,17 @@
          * context path /web-client, for built web-client assets
          * should get created.
          */
-        WITH_WEB_CLIENT
+        WITH_WEB_CLIENT,
+        /**
+         * Determines whether or not the connector at the given host
+         * and port will be TLS enabled or not.
+         */
+        WITH_TLS,
+        /**
+         * The path to the JKS keystore file with the keymaterial,
+         * alias 'thermostat', when WITH_TLS=true.
+         */
+        KEYSTORE_FILE,
     }
 
 }
--- a/distribution/src/etc/global-config.properties	Mon Jul 17 11:37:33 2017 +0200
+++ b/distribution/src/etc/global-config.properties	Mon Jul 17 11:45:37 2017 +0200
@@ -16,6 +16,22 @@
 # should get created.
 #
 WITH_WEB_CLIENT=true
+#
+# Specifies whether or not the configured connector at
+# IP:PORT will be TLS enabled or not. This will use
+# the keymaterial with alias "thermostat" in keystore
+# thermostat.jks
+#
+WITH_TLS=true
+#
+# Keystore file with the key material. Alias 'thermostat'
+# must be present. Relative paths resolve relative to
+# THERMOSTAT_GATEWAY_HOME. Specify the keystore password
+# via Java system property 'org.eclipse.jetty.ssl.password'
+# and the keymanager password via Java system property
+# 'org.eclipse.jetty.ssl.keypassword'
+#
+KEYSTORE_FILE=thermostat.jks
 
 MONGO_URL=mongodb://127.0.0.1:27518
 MONGO_DB=thermostat
--- a/server/src/main/java/com/redhat/thermostat/gateway/server/CoreServerBuilder.java	Mon Jul 17 11:37:33 2017 +0200
+++ b/server/src/main/java/com/redhat/thermostat/gateway/server/CoreServerBuilder.java	Mon Jul 17 11:45:37 2017 +0200
@@ -36,18 +36,25 @@
 
 package com.redhat.thermostat.gateway.server;
 
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Map;
 
 import javax.servlet.ServletException;
 
+import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
 import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
 
 import com.redhat.thermostat.gateway.common.core.config.Configuration;
@@ -60,11 +67,13 @@
 
 public class CoreServerBuilder {
 
+    private static final String THERMOSTAT_ALIAS = "thermostat";
     private final SwaggerUiHandler swaggerHandler;
     private final StaticAssetsHandler staticAssetsHandler;
     private final Server server = new Server();
     private CoreServiceBuilder coreServiceBuilder;
     private Configuration serverConfig;
+    private String gatewayHome;
 
     public CoreServerBuilder() {
         this(new SwaggerUiHandler(), new StaticAssetsHandler());
@@ -76,6 +85,8 @@
         this.swaggerHandler = swaggerHandler;
     }
 
+
+
     public CoreServerBuilder setServiceBuilder(CoreServiceBuilder builder) {
         this.coreServiceBuilder = builder;
         return this;
@@ -86,6 +97,11 @@
         return this;
     }
 
+    public CoreServerBuilder setGatewayHome(String gatewayHome) {
+        this.gatewayHome = gatewayHome;
+        return this;
+    }
+
     public Server build() {
         setupHandler();
         setupConnector();
@@ -134,19 +150,68 @@
     }
 
     private void setupConnector() {
+        Map<String, Object> serverConfigMap = serverConfig.asMap();
+        String listenAddress = (String)serverConfigMap.get(GlobalConfiguration.ConfigurationKey.IP.toString());
+        int listenPort = Integer.parseInt((String)serverConfigMap.get(GlobalConfiguration.ConfigurationKey.PORT.toString()));
+        Connector connector;
+        if (isEnabled(serverConfigMap, ConfigurationKey.WITH_TLS)) {
+            connector = getHttpsConnector(listenAddress, listenPort, serverConfigMap);
+        } else {
+            connector = getHttpConnector(listenAddress, listenPort);
+        }
+        server.setConnectors(new Connector[]{connector} );
+    }
+
+    private Connector getHttpsConnector(String listenAddress, int listenPort, Map<String, Object> serverConfigMap) {
+        String keystoreCandidate = (String)serverConfigMap.get((GlobalConfiguration.ConfigurationKey.KEYSTORE_FILE.name()));
+        Path keystoreFile = Paths.get(keystoreCandidate);
+        if (!keystoreFile.isAbsolute()) {
+            // resolve relative to GW home
+            keystoreFile = Paths.get(gatewayHome, keystoreCandidate);
+        }
+        ServerConnector connector = getPreconfiguredHttpsConnector(keystoreFile.toFile());
+        return configureHostPort(connector, listenAddress, listenPort);
+    }
+
+    private Connector getHttpConnector(String listenAddress, int listenPort) {
+        ServerConnector connector = getPreconfiguredHttpConnector();
+        return configureHostPort(connector, listenAddress, listenPort);
+    }
+
+    private ServerConnector configureHostPort(ServerConnector connector, String listenAddress, int listenPort) {
+        connector.setPort(listenPort);
+        connector.setHost(listenAddress);
+        return connector;
+    }
+
+    private ServerConnector getPreconfiguredHttpConnector() {
         ServerConnector httpConnector = new ServerConnector(server);
 
         HttpConfiguration httpConfig = new HttpConfiguration();
         httpConnector.addConnectionFactory(new HttpConnectionFactory(httpConfig));
+        return httpConnector;
+    }
 
-        Map<String, Object> serverConfigMap = serverConfig.asMap();
-        String listenAddress = (String)serverConfigMap.get(GlobalConfiguration.ConfigurationKey.IP.toString());
-        int listenPort = Integer.parseInt((String)serverConfigMap.get(GlobalConfiguration.ConfigurationKey.PORT.toString()));
+    private ServerConnector getPreconfiguredHttpsConnector(File keystoreFile) {
+        SslContextFactory sslContextFactory = new SslContextFactory();
+        sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath());
+        // May be overridden by setting the property -Dorg.eclipse.jetty.ssl.password=<password>
+        sslContextFactory.setKeyStorePassword("OBF:1sot1v961saj1v9i1v941sar1v9g1sox");
+        // May be overridden by setting the property -Dorg.eclipse.jetty.ssl.keypassword=<password>
+        sslContextFactory.setKeyManagerPassword("OBF:1sot1v961saj1v9i1v941sar1v9g1sox");
+        sslContextFactory.setCertAlias(THERMOSTAT_ALIAS);
 
-        httpConnector.setHost(listenAddress);
-        httpConnector.setPort(listenPort);
+        HttpConfiguration httpsConfig = new HttpConfiguration();
+        httpsConfig.setSecureScheme("https");
+        httpsConfig.setSecurePort(8443);
+        SecureRequestCustomizer src = new SecureRequestCustomizer();
+        httpsConfig.addCustomizer(src);
 
-        server.setConnectors(new Connector[]{httpConnector});
+        ServerConnector https = new ServerConnector(server,
+                new SslConnectionFactory(sslContextFactory,
+                        HttpVersion.HTTP_1_1.asString()),
+                new HttpConnectionFactory(httpsConfig));
+        return https;
     }
 
 }
--- a/server/src/main/java/com/redhat/thermostat/gateway/server/Start.java	Mon Jul 17 11:37:33 2017 +0200
+++ b/server/src/main/java/com/redhat/thermostat/gateway/server/Start.java	Mon Jul 17 11:45:37 2017 +0200
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.gateway.server;
 
 import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
 
 import com.redhat.thermostat.gateway.common.core.config.Configuration;
 import com.redhat.thermostat.gateway.common.core.config.ConfigurationFactory;
@@ -44,7 +45,6 @@
 import com.redhat.thermostat.gateway.server.services.CoreServiceBuilder;
 import com.redhat.thermostat.gateway.server.services.CoreServiceBuilderFactory;
 import com.redhat.thermostat.gateway.server.services.CoreServiceBuilderFactory.CoreServiceType;
-import org.eclipse.jetty.util.component.AbstractLifeCycle;
 
 public class Start implements Runnable {
 
@@ -68,6 +68,7 @@
         CoreServerBuilder serverBuilder = new CoreServerBuilder();
         setServerConfig(serverBuilder, factory);
         setServiceBuilder(serverBuilder, factory);
+        serverBuilder.setGatewayHome(gatewayHome);
 
         server = serverBuilder.build();
         if (listener != null) {
--- a/server/src/test/java/com/redhat/thermostat/gateway/server/CoreServerBuilderTest.java	Mon Jul 17 11:37:33 2017 +0200
+++ b/server/src/test/java/com/redhat/thermostat/gateway/server/CoreServerBuilderTest.java	Mon Jul 17 11:45:37 2017 +0200
@@ -37,17 +37,24 @@
 package com.redhat.thermostat.gateway.server;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.junit.Test;
@@ -78,6 +85,7 @@
         configMap.put(GlobalConfiguration.ConfigurationKey.PORT.name(), port);
         configMap.put(GlobalConfiguration.ConfigurationKey.WITH_SWAGGER_UI.name(), Boolean.FALSE.toString());
         configMap.put(GlobalConfiguration.ConfigurationKey.WITH_WEB_CLIENT.name(), Boolean.FALSE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_TLS.name(), Boolean.FALSE.toString());
         Configuration configuration = getMockConfiguration(configMap);
 
         CoreServerBuilder builder = new CoreServerBuilder();
@@ -110,6 +118,7 @@
         configMap.put(GlobalConfiguration.ConfigurationKey.PORT.name(), port);
         configMap.put(GlobalConfiguration.ConfigurationKey.WITH_SWAGGER_UI.name(), Boolean.TRUE.toString());
         configMap.put(GlobalConfiguration.ConfigurationKey.WITH_WEB_CLIENT.name(), Boolean.TRUE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_TLS.name(), Boolean.FALSE.toString());
         Configuration configuration = getMockConfiguration(configMap);
 
         SwaggerUiHandler swaggerHandler = new SwaggerUiHandler();
@@ -127,6 +136,58 @@
         assertEquals(3, handler.getHandlers().length);
     }
 
+    @Test
+    public void testHttpConnector() {
+        CoreServiceBuilder serviceBuilder = getMockServiceBuilder();
+
+        Map<String, Object> configMap = new HashMap<>();
+        String ip = "127.0.0.1";
+        String port = "8080";
+        configMap.put(GlobalConfiguration.ConfigurationKey.IP.name(), ip);
+        configMap.put(GlobalConfiguration.ConfigurationKey.PORT.name(), port);
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_SWAGGER_UI.name(), Boolean.FALSE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_WEB_CLIENT.name(), Boolean.FALSE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_TLS.name(), Boolean.FALSE.toString());
+        Configuration configuration = getMockConfiguration(configMap);
+        CoreServerBuilder builder = new CoreServerBuilder();
+        builder.setServerConfiguration(configuration);
+        builder.setServiceBuilder(serviceBuilder);
+
+        Server server = builder.build();
+        Connector[] connectors = server.getConnectors();
+        assertEquals(1, connectors.length);
+        assertTrue(connectors[0] instanceof ServerConnector);
+        ServerConnector serverConnector = (ServerConnector)connectors[0];
+        assertTrue(serverConnector.getDefaultConnectionFactory() instanceof HttpConnectionFactory);
+    }
+
+    @Test
+    public void testHttpsConnectorDefaultKeystoreLocation() {
+        CoreServiceBuilder serviceBuilder = getMockServiceBuilder();
+
+        Map<String, Object> configMap = new HashMap<>();
+        String ip = "127.0.0.1";
+        String port = "8080";
+        configMap.put(GlobalConfiguration.ConfigurationKey.IP.name(), ip);
+        configMap.put(GlobalConfiguration.ConfigurationKey.PORT.name(), port);
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_SWAGGER_UI.name(), Boolean.FALSE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_WEB_CLIENT.name(), Boolean.FALSE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.WITH_TLS.name(), Boolean.TRUE.toString());
+        configMap.put(GlobalConfiguration.ConfigurationKey.KEYSTORE_FILE.name(), "test_me.jks");
+        Configuration configuration = getMockConfiguration(configMap);
+        CoreServerBuilder builder = new CoreServerBuilder();
+        builder.setServerConfiguration(configuration);
+        builder.setServiceBuilder(serviceBuilder);
+        builder.setGatewayHome(getTestRoot("/test_gw_home"));
+
+        Server server = builder.build();
+        Connector[] connectors = server.getConnectors();
+        assertEquals(1, connectors.length);
+        assertTrue(connectors[0] instanceof ServerConnector);
+        ServerConnector serverConnector = (ServerConnector)connectors[0];
+        assertTrue(serverConnector.getDefaultConnectionFactory() instanceof SslConnectionFactory);
+    }
+
     private Configuration getMockConfiguration(Map<String, Object> configMap) {
         Configuration configuration = mock(Configuration.class);
         when(configuration.asMap()).thenReturn(configMap);
@@ -144,4 +205,19 @@
         when(serviceBuilder.build()).thenReturn(serviceList);
         return serviceBuilder;
     }
+
+    private String getTestRoot(String path) {
+        URL rootUrl = CoreServerBuilderTest.class.getResource(path);
+        return decodeFilePath(rootUrl);
+    }
+
+    private String decodeFilePath(URL url) {
+        try {
+            // Spaces are encoded as %20 in URLs - handle cases like that.
+            // requires Java 1.7
+            return Paths.get(url.toURI()).toFile().toString();
+        } catch (URISyntaxException e) {
+            throw new AssertionError("Syntax error in URI" + e.getMessage());
+        }
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/test/resources/test_gw_home/test_me.jks	Mon Jul 17 11:45:37 2017 +0200
@@ -0,0 +1,1 @@
+Not really a Java keystore file, but sufficient for the test.
\ No newline at end of file
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java	Mon Jul 17 11:37:33 2017 +0200
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java	Mon Jul 17 11:45:37 2017 +0200
@@ -41,18 +41,25 @@
 import java.nio.file.Paths;
 import java.util.concurrent.CountDownLatch;
 
-import com.redhat.thermostat.gateway.common.core.servlet.GlobalConstants;
-import com.redhat.thermostat.gateway.server.Start;
-
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.util.component.AbstractLifeCycle;
 import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 
+import com.redhat.thermostat.gateway.common.core.config.Configuration;
+import com.redhat.thermostat.gateway.common.core.config.ConfigurationFactory;
+import com.redhat.thermostat.gateway.common.core.config.GlobalConfiguration;
+import com.redhat.thermostat.gateway.common.core.servlet.GlobalConstants;
+import com.redhat.thermostat.gateway.server.Start;
+import com.redhat.thermostat.gateway.tests.utils.MongodTestUtil;
+
 public class IntegrationTest {
+    private static final ConfigurationFactory factory;
+
     protected static HttpClient client;
-    protected static String baseUrl = "http://127.0.0.1:30000";
+    protected static String baseUrl;
 
     protected static final Path distributionImage;
 
@@ -62,6 +69,14 @@
         if (distributionImage == null) {
             throw new RuntimeException("Environment variable THERMOSTAT_GATEWAY_HOME not defined!");
         }
+        factory = new ConfigurationFactory(distDir);
+        String scheme;
+        if (isTLSEnabled()) {
+            scheme = "https";
+        } else {
+            scheme = "http";
+        }
+        baseUrl = scheme + "://127.0.0.1:30000";
     }
 
     protected String resourceUrl;
@@ -72,12 +87,23 @@
 
     @BeforeClass
     public static void beforeClassIntegrationTest() throws Exception {
-        client = new HttpClient();
+        if (isTLSEnabled()) {
+            SslContextFactory sslFactory = new SslContextFactory();
+            sslFactory.setTrustAll(true);
+            client = new HttpClient(sslFactory);
+        } else {
+            client = new HttpClient();
+        }
         client.start();
 
         startServer();
     }
 
+    private static boolean isTLSEnabled() {
+        Configuration config = factory.createGlobalConfiguration();
+        return Boolean.parseBoolean((String)config.asMap().get(GlobalConfiguration.ConfigurationKey.WITH_TLS.name()));
+    }
+
     private static Thread serverThread = null;
     private static Start serverObject = null;