changeset 2722:8d8d281932c9

Make plugins TLS capable. Reviewed-by: ebaron Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/024128.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Fri, 14 Jul 2017 18:32:41 +0200
parents 1210fbb16bd5
children e109ac6f4bae
files agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpClientFacade.java agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacadeImpl.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java
diffstat 6 files changed, 181 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpClientFacade.java	Fri Jul 14 18:32:41 2017 +0200
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.agent.http;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.net.ssl.SSLContext;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import com.redhat.thermostat.common.ssl.SSLContextFactory;
+import com.redhat.thermostat.common.ssl.SslInitException;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
+
+class HttpClientFacade {
+
+    private static final Logger logger = LoggingUtils.getLogger(HttpClientFacade.class);
+    private static final long PER_REQUEST_TIMEOUT_SEC = 5;
+    private final HttpClient httpsClient;
+    
+    HttpClientFacade(SSLConfiguration sslConfig) {
+        httpsClient = createHttpsClient(sslConfig);
+    }
+
+    private static HttpClient createHttpsClient(SSLConfiguration config) {
+        try {
+            SSLContext context = SSLContextFactory.getClientContext(config);
+            SslContextFactory sslFactory = new SslContextFactory();
+            sslFactory.setSslContext(context);
+            // Don't send SSLv2 Client Hello. Some servers will refuse to
+            // accept it. So does the web-gateway with our self-signed cert.
+            sslFactory.setIncludeProtocols("TLSv1", "TLSv1.2");
+            if (config.disableHostnameVerification()) {
+                logger.fine("HTTPS endpoint verification disabled.");
+            } else {
+                sslFactory.setEndpointIdentificationAlgorithm("HTTPS");
+            }
+            HttpClient client = new HttpClient(sslFactory);
+            return client;
+        } catch (SslInitException e) {
+            logger.log(Level.INFO, "Failed to initialize SSL context.", e);
+            logger.severe("Failed to initialize SSL context. Reason: " + e.getMessage());
+            throw new RuntimeException(e);
+        } 
+    }
+    
+    void start() throws Exception {
+        httpsClient.start();
+    }
+    
+    Request newRequest(URI uri) {
+        return httpsClient.newRequest(uri).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
+    }
+    
+    Request newRequest(String url) {
+        return httpsClient.newRequest(url).timeout(PER_REQUEST_TIMEOUT_SEC, TimeUnit.SECONDS);
+    }
+}
--- a/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java	Fri Jul 14 15:44:08 2017 +0200
+++ b/agent/core/src/main/java/com/redhat/thermostat/agent/http/HttpRequestService.java	Fri Jul 14 18:32:41 2017 +0200
@@ -46,8 +46,8 @@
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.Service;
-import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
@@ -60,6 +60,7 @@
 import com.redhat.thermostat.agent.config.AgentStartupConfiguration;
 import com.redhat.thermostat.agent.http.internal.keycloak.KeycloakAccessToken;
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
 
 @Component
 @Service(value = HttpRequestService.class)
@@ -87,26 +88,29 @@
     private static final String KEYCLOAK_TOKEN_SERVICE = "/auth/realms/__REALM__/protocol/openid-connect/token";
     private static final String KEYCLOAK_CONTENT_TYPE = "application/x-www-form-urlencoded";
 
+    private final HttpClientCreator httpClientCreator;
     private Gson gson = new GsonBuilder().create();
-    private HttpClient client;
+    private HttpClientFacade client;
     private AgentStartupConfiguration agentStartupConfiguration;
 
     private KeycloakAccessToken keycloakAccessToken;
+    @Reference
+    private SSLConfiguration sslConfig;
 
     public HttpRequestService() {
-        this(new HttpClient(), AgentConfigsUtils.createAgentConfigs());
+        this(new HttpClientCreator(), AgentConfigsUtils.createAgentConfigs());
     }
 
-    HttpRequestService(HttpClient client, AgentStartupConfiguration agentStartupConfiguration) {
-        this.client = client;
+    HttpRequestService(HttpClientCreator clientCreator, AgentStartupConfiguration agentStartupConfiguration) {
+        this.httpClientCreator = clientCreator;
         this.agentStartupConfiguration = agentStartupConfiguration;
     }
 
     @Activate
     public void activate() {
         try {
+            client = httpClientCreator.create(sslConfig);
             client.start();
-
             logger.log(Level.FINE, "HttpRequestService activated");
         } catch (Exception e) {
             logger.log(Level.SEVERE, "HttpRequestService failed to start correctly. Behaviour undefined.", e);
@@ -218,6 +222,14 @@
                 "&refresh_token=" + keycloakAccessToken.getRefreshToken();
     }
     
+    static class HttpClientCreator {
+    
+        HttpClientFacade create(SSLConfiguration config) {
+            return new HttpClientFacade(config);
+        }
+
+    }
+    
     @SuppressWarnings("serial")
     public static class RequestFailedException extends Exception {
         
--- a/agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java	Fri Jul 14 15:44:08 2017 +0200
+++ b/agent/core/src/test/java/com/redhat/thermostat/agent/http/HttpRequestServiceTest.java	Fri Jul 14 18:32:41 2017 +0200
@@ -41,8 +41,8 @@
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -53,7 +53,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
-import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
@@ -64,7 +63,9 @@
 import org.mockito.ArgumentCaptor;
 
 import com.redhat.thermostat.agent.config.AgentStartupConfiguration;
+import com.redhat.thermostat.agent.http.HttpRequestService.HttpClientCreator;
 import com.redhat.thermostat.agent.http.HttpRequestService.RequestFailedException;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
 
 public class HttpRequestServiceTest {
     private static final String POST_METHOD = HttpRequestService.POST;
@@ -73,24 +74,27 @@
     private static final String payload = "{}";
     private static final String keycloakUrl = "http://127.0.0.1:31000/keycloak";
 
-    private HttpClient client;
+    private HttpClientCreator clientCreator;
+    private HttpClientFacade client;
     private Request httpRequest;
-
+    
     @Before
     public void setup() throws InterruptedException, ExecutionException, TimeoutException {
-        client = mock(HttpClient.class);
+        client = mock(HttpClientFacade.class);
         httpRequest = mock(Request.class);
         when(client.newRequest(eq(URL))).thenReturn(httpRequest);
         ContentResponse response = mock(ContentResponse.class);
         when(response.getStatus()).thenReturn(HttpStatus.OK_200);
         when(httpRequest.send()).thenReturn(response);
+        clientCreator = mock(HttpClientCreator.class);
+        when(clientCreator.create(any(SSLConfiguration.class))).thenReturn(client);
     }
 
     @Test
     public void testRequestWithoutKeycloak() throws Exception {
         AgentStartupConfiguration configuration = createNoKeycloakConfig();
 
-        HttpRequestService service = new HttpRequestService(client, configuration);
+        HttpRequestService service = createAndActivateRequestService(configuration);
 
         service.sendHttpRequest(payload, URL, POST_METHOD);
 
@@ -103,7 +107,7 @@
         AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
         setupKeycloakConfig(configuration);
 
-        HttpRequestService service = new HttpRequestService(client, configuration);
+        HttpRequestService service = createAndActivateRequestService(configuration);
 
         Request keycloakRequest = mock(Request.class);
         setupKeycloakRequest(keycloakRequest);
@@ -122,7 +126,7 @@
         AgentStartupConfiguration configuration = mock(AgentStartupConfiguration.class);
         setupKeycloakConfig(configuration);
 
-        HttpRequestService service = new HttpRequestService(client, configuration);
+        HttpRequestService service = createAndActivateRequestService(configuration);
 
         Request keycloakRequest = mock(Request.class);
         setupKeycloakRequest(keycloakRequest);
@@ -158,7 +162,7 @@
     public void testRequestWithNullPayload() throws Exception {
         AgentStartupConfiguration configuration = createNoKeycloakConfig();
 
-        HttpRequestService service = new HttpRequestService(client, configuration);
+        HttpRequestService service = createAndActivateRequestService(configuration);
 
         String response = service.sendHttpRequest(null, URL, POST_METHOD);
         assertNull(response);
@@ -170,6 +174,13 @@
         verify(httpRequest).method(eq(HttpMethod.valueOf(POST_METHOD)));
         verify(httpRequest).send();
     }
+    
+    private HttpRequestService createAndActivateRequestService(AgentStartupConfiguration configuration) throws Exception {
+        HttpRequestService service = new HttpRequestService(clientCreator, configuration);
+        service.activate();
+        verify(client).start();
+        return service;
+    }
 
     @Test
     public void testGetRequestWithResponse() throws Exception {
@@ -179,11 +190,14 @@
         when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
         when(contentResponse.getContentAsString()).thenReturn(getContent);
         when(request.send()).thenReturn(contentResponse);
-        HttpClient getClient = mock(HttpClient.class);
+        HttpClientFacade getClient = mock(HttpClientFacade.class);
         when(getClient.newRequest(eq(GET_URL))).thenReturn(request);
+        HttpClientCreator creator = mock(HttpClientCreator.class);
+        when(creator.create(any(SSLConfiguration.class))).thenReturn(getClient);
         
         AgentStartupConfiguration configuration = createNoKeycloakConfig();
-        HttpRequestService service = new HttpRequestService(getClient, configuration);
+        HttpRequestService service = new HttpRequestService(creator, configuration);
+        service.activate();
         String content = service.sendHttpRequest(null, GET_URL, HttpRequestService.GET);
         assertEquals(getContent, content);
     }
@@ -194,7 +208,7 @@
         when(client.newRequest(any(String.class))).thenReturn(request);
         AgentStartupConfiguration configuration = createNoKeycloakConfig();
         doThrow(IOException.class).when(request).send();
-        HttpRequestService service = new HttpRequestService(client, configuration);
+        HttpRequestService service = createAndActivateRequestService(configuration);
         service.sendHttpRequest("foo", "bar", HttpRequestService.DELETE /*any valid method*/);
     }
 
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Fri Jul 14 15:44:08 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Fri Jul 14 18:32:41 2017 +0200
@@ -64,6 +64,7 @@
 import com.redhat.thermostat.common.plugin.PluginConfiguration;
 import com.redhat.thermostat.common.utils.LoggingUtils;
 import com.redhat.thermostat.shared.config.CommonPaths;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
 import com.redhat.thermostat.storage.config.FileStorageCredentials;
 import com.redhat.thermostat.storage.core.StorageCredentials;
 import com.redhat.thermostat.storage.core.WriterID;
@@ -98,6 +99,9 @@
 
     @Reference
     private ConfigurationInfoSource commandInfo;
+    
+    @Reference
+    private SSLConfiguration sslConfig;
 
     public CommandsBackend() {
         this(new WsClientCreator(), new CredentialsCreator(), new ConfigCreator(), new CountDownLatch(1));
@@ -154,7 +158,7 @@
         creds = credsCreator.create(paths);
         config = configCreator.createConfig(commandInfo);
         try {
-            wsClient = wsClientCreator.createClient();
+            wsClient = wsClientCreator.createClient(sslConfig);
             wsClient.start();
         } catch (Exception e) {
             logger.log(Level.WARNING,
@@ -234,8 +238,8 @@
     }
     
     static class WsClientCreator {
-        WebSocketClientFacade createClient() {
-            return new WebSocketClientFacadeImpl();
+        WebSocketClientFacade createClient(SSLConfiguration sslConfig) {
+            return new WebSocketClientFacadeImpl(sslConfig);
         }
     }
     
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacadeImpl.java	Fri Jul 14 15:44:08 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacadeImpl.java	Fri Jul 14 18:32:41 2017 +0200
@@ -41,11 +41,17 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.net.ssl.SSLContext;
+
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 import org.eclipse.jetty.websocket.client.WebSocketClient;
 
 import com.redhat.thermostat.commands.agent.internal.socket.CmdChannelAgentSocket;
+import com.redhat.thermostat.common.ssl.SSLContextFactory;
+import com.redhat.thermostat.common.ssl.SslInitException;
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
 
 class WebSocketClientFacadeImpl implements WebSocketClientFacade {
     
@@ -53,8 +59,8 @@
     private final WebSocketClient client;
     private CmdChannelAgentSocket socket;
     
-    WebSocketClientFacadeImpl() {
-        this.client = new WebSocketClient();
+    WebSocketClientFacadeImpl(SSLConfiguration sslConfig) {
+        this.client = createWebSocketClient(sslConfig);
     }
 
     @Override
@@ -81,5 +87,27 @@
         }
         client.destroy();
     }
+    
+    private static WebSocketClient createWebSocketClient(SSLConfiguration config) {
+        try {
+            SSLContext context = SSLContextFactory.getClientContext(config);
+            SslContextFactory sslFactory = new SslContextFactory();
+            sslFactory.setSslContext(context);
+            // Don't send SSLv2 Client Hello. Some servers will refuse to
+            // accept it. So does the web-gateway with our self-signed cert.
+            sslFactory.setIncludeProtocols("TLSv1", "TLSv1.2");
+            if (config.disableHostnameVerification()) {
+                logger.fine("HTTPS endpoint verification disabled.");
+            } else {
+                sslFactory.setEndpointIdentificationAlgorithm("HTTPS");
+            }
+            return new WebSocketClient(sslFactory);
+        } catch (SslInitException e) {
+            logger.log(Level.INFO, "Failed to initialize SSL context.", e);
+            logger.severe("Failed to initialize SSL context. Reason: "
+                    + e.getMessage());
+            throw new RuntimeException(e);
+        }
+    }
 
 }
--- a/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java	Fri Jul 14 15:44:08 2017 +0200
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java	Fri Jul 14 18:32:41 2017 +0200
@@ -65,6 +65,7 @@
 import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource;
 import com.redhat.thermostat.common.plugin.PluginConfiguration;
 import com.redhat.thermostat.shared.config.CommonPaths;
+import com.redhat.thermostat.shared.config.SSLConfiguration;
 import com.redhat.thermostat.storage.core.StorageCredentials;
 
 public class CommandsBackendTest {
@@ -84,7 +85,7 @@
         when(credsCreator.create(any(CommonPaths.class))).thenReturn(creds);
         WsClientCreator creator = mock(WsClientCreator.class);
         client = mock(WebSocketClientFacade.class);
-        when(creator.createClient()).thenReturn(client);
+        when(creator.createClient(any(SSLConfiguration.class))).thenReturn(client);
         ConfigCreator configCreator = mock(ConfigCreator.class);
         PluginConfiguration config = mock(PluginConfiguration.class);
         when(config.getGatewayURL()).thenReturn(GW_URL);