Mercurial > hg > thermostat-ng > agent
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);