# HG changeset patch # User Severin Gehwolf # Date 1359629623 -3600 # Node ID 4aa310fa7589380962f45ab08b285adc79cd50d4 # Parent 4c4b627b3f9cb83a3cb5d542dc3593593797c169 Add mongodb client TLS support. Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-January/005226.html PR1243 diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfiguration.java --- a/common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfiguration.java Tue Jan 29 10:55:15 2013 -0500 +++ b/common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfiguration.java Thu Jan 31 11:53:43 2013 +0100 @@ -50,6 +50,7 @@ private static final String KEYSTORE_FILE_KEY = "KEYSTORE_FILE"; private static final String KEYSTORE_FILE_PWD_KEY = "KEYSTORE_PASSWORD"; private static final String CMD_CHANNEL_SSL_KEY = "COMMAND_CHANNEL_USE_SSL"; + private static final String MONGO_CONNECTION_USE_SSL_KEY = "MONGODB_CONNECTION_USE_SSL"; /** * @@ -113,6 +114,27 @@ } return result; } + + /** + * + * @return true if and only if SSL should be used for mongodb connections on + * client side. I.e. if $THERMOSTAT_HOME/etc/ssl.properties exists + * and proper config has been added. false otherwise. + */ + public static boolean useSslForMongodb() { + boolean result = false; + try { + loadClientProperties(); + } catch (InvalidConfigurationException e) { + // Thermostat home not set? Do something reasonable + return result; + } + String token = clientProps.getProperty(MONGO_CONNECTION_USE_SSL_KEY); + if (token != null) { + result = Boolean.parseBoolean(token); + } + return result; + } // testing hook static void initClientProperties(File clientPropertiesFile) { diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortPair.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortPair.java Thu Jan 31 11:53:43 2013 +0100 @@ -0,0 +1,56 @@ +/* + * Copyright 2012, 2013 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 + * . + * + * 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.common.utils; + +public class HostPortPair { + private String host; + private int port; + + public HostPortPair(String host, int port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } +} + diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortsParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortsParser.java Thu Jan 31 11:53:43 2013 +0100 @@ -0,0 +1,120 @@ +/* + * Copyright 2012, 2013 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 + * . + * + * 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.common.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses Host/Port pairs from a raw string. + * + *
+ * IPv4:
+ *      127.0.0.1:9999,127.0.0.2:8888
+ * 
+ * + * or + * + *
+ * IPv6:
+ *      [1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001
+ * 
+ * + * or + * + *
+ * DNS hostname:port pairs:
+ *      testhost.example.com:8970,host2.example.com:1234
+ * 
+ * + * Be sure to call {@link #parse()} before getting the list of + * host/port pairs via {@link #getHostsPorts()}. + */ +public class HostPortsParser { + + private final String rawString; + private List ipPorts; + private final IllegalArgumentException formatException; + + public HostPortsParser(String parseString) { + this.rawString = parseString; + this.formatException = new IllegalArgumentException("Invalid format of IP/port argument " + rawString); + } + + public void parse() throws IllegalArgumentException { + ipPorts = new ArrayList<>(); + for (String ipPortPair: rawString.split(",")) { + // if we have a '[' in the ip:port pair string we likely have an IPv6 + int idxRparen = ipPortPair.indexOf(']'); + int idxLParen = ipPortPair.indexOf('['); + if (idxLParen == -1) { + // IPv4 + if (idxRparen != -1 || ipPortPair.indexOf(':') == -1) { + throw formatException; + } + String[] ipPort = ipPortPair.split(":"); + int port = -1; + try { + port = Integer.parseInt(ipPort[1]); + } catch (NumberFormatException e) { + throw formatException; + } + ipPorts.add(new HostPortPair(ipPort[0], port)); + } else { + // IPv6 + if (idxRparen == -1) { + throw formatException; + } + int port = -1; + try { + port = Integer.parseInt(ipPortPair.substring(idxRparen + 2)); + } catch (NumberFormatException e) { + throw formatException; + } + ipPorts.add(new HostPortPair(ipPortPair.substring(idxLParen + 1, idxRparen), port)); + } + } + } + + public List getHostsPorts() { + if (ipPorts == null) { + throw new IllegalStateException("Must call parse() before getting map!"); + } + return ipPorts; + } +} + diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfigurationTest.java --- a/common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfigurationTest.java Tue Jan 29 10:55:15 2013 -0500 +++ b/common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfigurationTest.java Thu Jan 31 11:53:43 2013 +0100 @@ -67,13 +67,15 @@ } @Test - public void canGetSSLEnabledConfig() { + public void canGetSSLEnabledConfigs() { File clientProps = new File(this.getClass().getResource("/client.properties").getFile()); SSLKeystoreConfiguration.initClientProperties(clientProps); assertTrue(SSLKeystoreConfiguration.shouldSSLEnableCmdChannel()); + assertTrue(SSLKeystoreConfiguration.useSslForMongodb()); clientProps = new File(this.getClass().getResource("/ssl.properties").getFile()); SSLKeystoreConfiguration.initClientProperties(clientProps); assertFalse(SSLKeystoreConfiguration.shouldSSLEnableCmdChannel()); + assertFalse(SSLKeystoreConfiguration.useSslForMongodb()); } } diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/test/java/com/redhat/thermostat/common/utils/HostPortsParserTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/core/src/test/java/com/redhat/thermostat/common/utils/HostPortsParserTest.java Thu Jan 31 11:53:43 2013 +0100 @@ -0,0 +1,127 @@ +/* + * Copyright 2012, 2013 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 + * . + * + * 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.common.utils; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import com.redhat.thermostat.common.utils.HostPortPair; +import com.redhat.thermostat.common.utils.HostPortsParser; + +public class HostPortsParserTest { + + @Test + public void canParseIpV4Pair() throws IllegalArgumentException { + HostPortsParser parser = new HostPortsParser( + "127.0.0.1:8080,127.0.0.1:9999"); + parser.parse(); + List ipPorts = parser.getHostsPorts(); + assertEquals(2, ipPorts.size()); + assertEquals(8080, (long) ipPorts.get(0).getPort()); + assertEquals("127.0.0.1", ipPorts.get(0).getHost()); + assertEquals(9999, (long) ipPorts.get(1).getPort()); + assertEquals("127.0.0.1", ipPorts.get(1).getHost()); + } + + @Test + public void canParseDnsHostsPortsPair() throws IllegalArgumentException { + HostPortsParser parser = new HostPortsParser( + "somehost.example.com:8080,host2.example.com:9999"); + parser.parse(); + List ipPorts = parser.getHostsPorts(); + assertEquals(2, ipPorts.size()); + assertEquals(8080, (long) ipPorts.get(0).getPort()); + assertEquals("somehost.example.com", ipPorts.get(0).getHost()); + assertEquals(9999, (long) ipPorts.get(1).getPort()); + assertEquals("host2.example.com", ipPorts.get(1).getHost()); + parser = new HostPortsParser( + "thermostat-storage.fluff.org:9999"); + parser.parse(); + HostPortPair pair = parser.getHostsPorts().get(0); + assertEquals("thermostat-storage.fluff.org", pair.getHost()); + assertEquals(9999, pair.getPort()); + } + + @Test + public void canParseIpv6Pair() { + HostPortsParser parser = new HostPortsParser( + "[1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001"); + parser.parse(); + List ipPorts = parser.getHostsPorts(); + assertEquals(2, ipPorts.size()); + assertEquals(8001, (long) ipPorts.get(0).getPort()); + assertEquals("1fff:0:a88:85a3::ac1f", ipPorts.get(0).getHost()); + assertEquals(8001, (long) ipPorts.get(1).getPort()); + assertEquals("1fff:0:a88:85a3::ac2f", ipPorts.get(1).getHost()); + } + + @Test + public void failsParsingInvalidString() { + HostPortsParser parser = new HostPortsParser( + "1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001"); + int expectedExcptns = 3; + int exptns = 0; + try { + parser.parse(); + } catch (IllegalArgumentException e) { + exptns++; + } + parser = new HostPortsParser("blah,test"); + try { + parser.parse(); + } catch (IllegalArgumentException e) { + exptns++; + } + parser = new HostPortsParser("127.0.0.1:80,127.0.0.2:bad"); + try { + parser.parse(); + } catch (IllegalArgumentException e) { + exptns++; + } + assertEquals(expectedExcptns, exptns); + } + + @Test(expected = IllegalStateException.class) + public void getMapWithNoParseThrowsException() { + HostPortsParser parser = new HostPortsParser("blah"); + parser.getHostsPorts(); + } +} + diff -r 4c4b627b3f9c -r 4aa310fa7589 common/core/src/test/resources/client.properties --- a/common/core/src/test/resources/client.properties Tue Jan 29 10:55:15 2013 -0500 +++ b/common/core/src/test/resources/client.properties Thu Jan 31 11:53:43 2013 +0100 @@ -1,4 +1,5 @@ # Random comment KEYSTORE_FILE=/path/to/thermostat.keystore KEYSTORE_PASSWORD=some password -COMMAND_CHANNEL_USE_SSL=true \ No newline at end of file +COMMAND_CHANNEL_USE_SSL=true +MONGODB_CONNECTION_USE_SSL=true \ No newline at end of file diff -r 4c4b627b3f9c -r 4aa310fa7589 distribution/config/db.properties --- a/distribution/config/db.properties Tue Jan 29 10:55:15 2013 -0500 +++ b/distribution/config/db.properties Thu Jan 31 11:53:43 2013 +0100 @@ -42,6 +42,20 @@ ################################################################### # SSL configuration ################################################################### +# +# NOTE: Enabling this config will likely require +# MONGODB_CONNECTION_USE_SSL=true to be set in +# $THERMOSTAT_HOME/etc/ssl.properties for thermostat client +# components which wish to establish a SSL connection to the +# mongodb storage server. These components include, but are +# not limited to, web service, agent, gui, shell (all of +# which make use of ssl.properties and settings within). +# In ssl.properties an appropriate thermostat - or system - +# keystore needs to be configured which has the certificate +# as specified here in a trusted keychain. +# Configuration in this file is only required in order to +# start mongodb (the server component) with SSL enabled. +# # Uncomment the following line in order to start storage (currently # mongodb) with SSL enabled. #SSL_ENABLE=true @@ -55,4 +69,3 @@ # needs to be specified. If the server key was not encrypted any # non-empty password will work. #SSL_KEY_PASSWORD=somepassword - diff -r 4c4b627b3f9c -r 4aa310fa7589 distribution/config/ssl.properties --- a/distribution/config/ssl.properties Tue Jan 29 10:55:15 2013 -0500 +++ b/distribution/config/ssl.properties Thu Jan 31 11:53:43 2013 +0100 @@ -15,4 +15,10 @@ # channel communication. Note that if this is set to true, both of the above # configs are required on the agent host, since it will use the key material # in the keystore file for SSL handshakes. -#COMMAND_CHANNEL_USE_SSL=true \ No newline at end of file +#COMMAND_CHANNEL_USE_SSL=true + +# Uncomment the following line if mongodb connections need to use SSL. I.e. +# enable this if you are configuring a thermostat client component which +# needs to do a SSL handshake with mongodb storage. See SSL_ENABLE in +# $THERMOSTAT_HOME/storage/db.properties). +#MONGODB_CONNECTION_USE_SSL=true diff -r 4c4b627b3f9c -r 4aa310fa7589 storage/core/pom.xml --- a/storage/core/pom.xml Tue Jan 29 10:55:15 2013 -0500 +++ b/storage/core/pom.xml Thu Jan 31 11:53:43 2013 +0100 @@ -111,6 +111,11 @@ ${project.version} test + + com.redhat.thermostat + thermostat-common-core + ${project.version} + diff -r 4c4b627b3f9c -r 4aa310fa7589 storage/mongo/pom.xml --- a/storage/mongo/pom.xml Tue Jan 29 10:55:15 2013 -0500 +++ b/storage/mongo/pom.xml Thu Jan 31 11:53:43 2013 +0100 @@ -117,6 +117,11 @@ thermostat-storage-core ${project.version} + + com.redhat.thermostat + thermostat-common-core + ${project.version} + diff -r 4c4b627b3f9c -r 4aa310fa7589 storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java --- a/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java Tue Jan 29 10:55:15 2013 -0500 +++ b/storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java Thu Jan 31 11:53:43 2013 +0100 @@ -38,11 +38,22 @@ import java.io.IOException; import java.net.UnknownHostException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLContext; import com.mongodb.DB; import com.mongodb.Mongo; import com.mongodb.MongoException; -import com.mongodb.MongoURI; +import com.mongodb.MongoOptions; +import com.mongodb.ServerAddress; +import com.redhat.thermostat.common.ssl.SSLContextFactory; +import com.redhat.thermostat.common.ssl.SSLKeystoreConfiguration; +import com.redhat.thermostat.common.ssl.SslInitException; +import com.redhat.thermostat.common.utils.HostPortPair; +import com.redhat.thermostat.common.utils.HostPortsParser; +import com.redhat.thermostat.common.utils.LoggingUtils; import com.redhat.thermostat.storage.config.AuthenticationConfiguration; import com.redhat.thermostat.storage.config.StartupConfiguration; import com.redhat.thermostat.storage.core.Connection; @@ -50,6 +61,7 @@ class MongoConnection extends Connection { + private static final Logger logger = LoggingUtils.getLogger(MongoConnection.class); static final String THERMOSTAT_DB_NAME = "thermostat"; private Mongo m = null; @@ -70,7 +82,7 @@ connected = true; } catch (IOException | MongoException | IllegalArgumentException e) { - e.printStackTrace(); + logger.log(Level.WARNING, "Failed to connect to storage", e); fireChanged(ConnectionStatus.FAILED_TO_CONNECT); throw new ConnectionException(e.getMessage(), e); } @@ -107,18 +119,45 @@ } private void createConnection() throws MongoException, UnknownHostException { - this.m = new Mongo(getMongoURI()); + if (SSLKeystoreConfiguration.useSslForMongodb()) { + this.m = getSSLMongo(); + } else { + this.m = new Mongo(getServerAddress()); + } this.db = m.getDB(THERMOSTAT_DB_NAME); } - private MongoURI getMongoURI() { + Mongo getSSLMongo() throws UnknownHostException, MongoException { + MongoOptions opts = new MongoOptions(); + SSLContext ctxt = null; + try { + ctxt = SSLContextFactory.getClientContext(); + } catch (SslInitException e) { + logger.log(Level.WARNING, "Failed to get SSL context!", e); + throw new MongoException(e.getMessage(), e); + } + opts.socketFactory = ctxt.getSocketFactory(); + return new Mongo(getServerAddress(), opts); + } + + ServerAddress getServerAddress() throws UnknownHostException { String url = conf.getDBConnectionString(); - MongoURI uri = new MongoURI(url); - return uri; + // Strip mongodb prefix: "mongodb://".length() == 10 + String hostPort = url.substring(10); + HostPortsParser parser = new HostPortsParser(hostPort); + parser.parse(); + HostPortPair ipPort = parser.getHostsPorts().get(0); + ServerAddress addr = new ServerAddress(ipPort.getHost(), ipPort.getPort()); + return addr; } private void testConnection() { db.getCollection("agent-config").getCount(); } + + // Testing hook + Mongo getMongo() { + return this.m; + } } diff -r 4c4b627b3f9c -r 4aa310fa7589 storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java --- a/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java Tue Jan 29 10:55:15 2013 -0500 +++ b/storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java Thu Jan 31 11:53:43 2013 +0100 @@ -36,19 +36,30 @@ package com.redhat.thermostat.storage.mongodb.internal; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; import java.io.IOException; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @@ -56,15 +67,23 @@ import com.mongodb.DBCollection; import com.mongodb.Mongo; import com.mongodb.MongoException; +import com.mongodb.MongoOptions; import com.mongodb.MongoURI; +import com.mongodb.ServerAddress; +import com.redhat.thermostat.common.ssl.SSLContextFactory; +import com.redhat.thermostat.common.ssl.SSLKeystoreConfiguration; import com.redhat.thermostat.storage.config.StartupConfiguration; -import com.redhat.thermostat.storage.core.ConnectionException; import com.redhat.thermostat.storage.core.Connection.ConnectionListener; import com.redhat.thermostat.storage.core.Connection.ConnectionStatus; -import com.redhat.thermostat.storage.mongodb.internal.MongoConnection; +import com.redhat.thermostat.storage.core.ConnectionException; -@PrepareForTest(MongoConnection.class) @RunWith(PowerMockRunner.class) +// There is a bug (resolved as wontfix) in powermock which results in +// java.lang.LinkageError if javax.management.* classes aren't ignored by +// Powermock. More here: http://code.google.com/p/powermock/issues/detail?id=277 +// SSL tests need this and having that annotation on method level doesn't seem +// to solve the issue. +@PowerMockIgnore( {"javax.management.*"}) public class MongoConnectionTest { private MongoConnection conn; @@ -84,6 +103,7 @@ conn = null; } + @PrepareForTest({ MongoConnection.class }) @Test public void testConnectSuccess() throws Exception { DBCollection collection = mock(DBCollection.class); @@ -97,6 +117,7 @@ verify(listener).changed(ConnectionStatus.CONNECTED); } + @PrepareForTest({ MongoConnection.class }) @Test public void testConnectIOException() throws Exception { PowerMockito.whenNew(Mongo.class).withParameterTypes(MongoURI.class).withArguments(any(MongoURI.class)).thenThrow(new IOException()); @@ -110,9 +131,10 @@ assertTrue(exceptionThrown); } + @PrepareForTest({ MongoConnection.class }) @Test public void testConnectMongoException() throws Exception { - PowerMockito.whenNew(Mongo.class).withParameterTypes(MongoURI.class).withArguments(any(MongoURI.class)).thenThrow(new MongoException("fluff")); + PowerMockito.whenNew(Mongo.class).withParameterTypes(ServerAddress.class).withArguments(any(ServerAddress.class)).thenThrow(new MongoException("fluff")); boolean exceptionThrown = false; try { conn.connect(); @@ -123,5 +145,82 @@ verify(listener).changed(ConnectionStatus.FAILED_TO_CONNECT); assertTrue(exceptionThrown); } + + @PrepareForTest({ MongoConnection.class, SSLKeystoreConfiguration.class, + SSLContextFactory.class, SSLContext.class, SSLSocketFactory.class }) + @Test + public void verifySSLSocketFactoryUsedIfSSLEnabled() throws Exception { + PowerMockito.mockStatic(SSLKeystoreConfiguration.class); + when(SSLKeystoreConfiguration.useSslForMongodb()).thenReturn(true); + + PowerMockito.mockStatic(SSLContextFactory.class); + // SSL classes need to be mocked with PowerMockito + SSLContext context = PowerMockito.mock(SSLContext.class); + when(SSLContextFactory.getClientContext()).thenReturn(context); + SSLSocketFactory factory = PowerMockito.mock(SSLSocketFactory.class); + when(context.getSocketFactory()).thenReturn(factory); + Mongo mockMongo = mock(Mongo.class); + ArgumentCaptor mongoOptCaptor = ArgumentCaptor.forClass(MongoOptions.class); + whenNew(Mongo.class).withParameterTypes(ServerAddress.class, + MongoOptions.class).withArguments(any(ServerAddress.class), + mongoOptCaptor.capture()).thenReturn(mockMongo); + DB mockDb = mock(DB.class); + when(mockMongo.getDB(eq(MongoConnection.THERMOSTAT_DB_NAME))).thenReturn(mockDb); + DBCollection mockCollection = mock(DBCollection.class); + when(mockDb.getCollection(any(String.class))).thenReturn(mockCollection); + conn.connect(); + Mongo mongo = conn.getMongo(); + assertEquals(mockMongo, mongo); + MongoOptions opts = mongoOptCaptor.getValue(); + assertTrue(opts.socketFactory instanceof SSLSocketFactory); + assertEquals(factory, opts.socketFactory); + } + + @PrepareForTest({ SSLKeystoreConfiguration.class, + SSLContextFactory.class, SSLContext.class, SSLSocketFactory.class }) + @Test + public void verifyNoSSLSocketFactoryUsedIfSSLDisabled() throws Exception { + PowerMockito.mockStatic(SSLKeystoreConfiguration.class); + when(SSLKeystoreConfiguration.useSslForMongodb()).thenReturn(false); + + MongoConnection connection = mock(MongoConnection.class); + connection.connect(); + verify(connection, Mockito.times(0)).getSSLMongo(); + } + + @Test + public void canGetServerAddress() { + StartupConfiguration config = new StartupConfiguration() { + + @Override + public String getDBConnectionString() { + return "mongodb://127.0.1.1:23452"; + } + }; + MongoConnection connection = new MongoConnection(config); + ServerAddress addr = null; + try { + addr = connection.getServerAddress(); + } catch (UnknownHostException e) { + fail("Should not have thrown exception!"); + } + assertEquals(23452, addr.getPort()); + assertEquals("127.0.1.1", addr.getHost()); + + config = new StartupConfiguration() { + + @Override + public String getDBConnectionString() { + return "fluff://willnotwork.com:23452"; + } + }; + connection = new MongoConnection(config); + try { + connection.getServerAddress(); + fail("should not have been able to parse address"); + } catch (UnknownHostException e) { + // pass + } + } } diff -r 4c4b627b3f9c -r 4aa310fa7589 web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceCommand.java --- a/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceCommand.java Tue Jan 29 10:55:15 2013 -0500 +++ b/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceCommand.java Thu Jan 31 11:53:43 2013 +0100 @@ -42,8 +42,8 @@ import com.redhat.thermostat.common.cli.CommandContext; import com.redhat.thermostat.common.cli.CommandException; import com.redhat.thermostat.common.cli.AbstractCommand; -import com.redhat.thermostat.web.server.IpPortPair; -import com.redhat.thermostat.web.server.IpPortsParser; +import com.redhat.thermostat.common.utils.HostPortPair; +import com.redhat.thermostat.common.utils.HostPortsParser; public class WebServiceCommand extends AbstractCommand { @@ -99,14 +99,14 @@ return false; } - private List parseIPsPorts(String rawIpsPorts) throws CommandException { - IpPortsParser parser = new IpPortsParser(rawIpsPorts); + private List parseIPsPorts(String rawIpsPorts) throws CommandException { + HostPortsParser parser = new HostPortsParser(rawIpsPorts); try { parser.parse(); } catch (IllegalArgumentException e) { throw new CommandException(e); } - return parser.getIpsPorts(); + return parser.getHostsPorts(); } } diff -r 4c4b627b3f9c -r 4aa310fa7589 web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java --- a/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java Tue Jan 29 10:55:15 2013 -0500 +++ b/web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java Thu Jan 31 11:53:43 2013 +0100 @@ -54,8 +54,8 @@ import org.eclipse.jetty.webapp.WebAppContext; import com.redhat.thermostat.common.config.InvalidConfigurationException; +import com.redhat.thermostat.common.utils.HostPortPair; import com.redhat.thermostat.storage.mongodb.MongoStorageProvider; -import com.redhat.thermostat.web.server.IpPortPair; import com.redhat.thermostat.web.server.WebStorageEndPoint; class WebServiceLauncher { @@ -65,7 +65,7 @@ private String storageUsername; private String storagePassword; // IP/Port pairs, keyed by IP - private List ipsPorts; + private List ipsPorts; WebServiceLauncher() { server = new Server(); @@ -80,10 +80,10 @@ checkConfig(); Connector[] connectors = new Connector[ipsPorts.size()]; for (int i = 0; i < ipsPorts.size(); i++) { - IpPortPair pair = ipsPorts.get(i); + HostPortPair pair = ipsPorts.get(i); connectors[i] = new SelectChannelConnector(); connectors[i].setPort(pair.getPort()); - connectors[i].setHost(pair.getIp()); + connectors[i].setHost(pair.getHost()); } server.setConnectors( connectors ); @@ -153,7 +153,7 @@ this.storagePassword = storagePassword; } - public void setIpAddresses(List ipsPorts) { + public void setIpAddresses(List ipsPorts) { this.ipsPorts = ipsPorts; } @@ -167,7 +167,7 @@ if (ipsPorts == null) { throw new InvalidConfigurationException("IP adresses to bind to must be set"); } - for (IpPortPair pair: ipsPorts) { + for (HostPortPair pair: ipsPorts) { if (pair.getPort() <= 0) { throw new InvalidConfigurationException("Invalid port number " + pair.getPort()); } diff -r 4c4b627b3f9c -r 4aa310fa7589 web/cmd/src/test/java/com/redhat/thermostat/web/cmd/WebServiceLauncherTest.java --- a/web/cmd/src/test/java/com/redhat/thermostat/web/cmd/WebServiceLauncherTest.java Tue Jan 29 10:55:15 2013 -0500 +++ b/web/cmd/src/test/java/com/redhat/thermostat/web/cmd/WebServiceLauncherTest.java Thu Jan 31 11:53:43 2013 +0100 @@ -50,17 +50,17 @@ import org.junit.Test; import com.redhat.thermostat.common.config.InvalidConfigurationException; -import com.redhat.thermostat.web.server.IpPortPair; +import com.redhat.thermostat.common.utils.HostPortPair; public class WebServiceLauncherTest { private WebServiceLauncher launcher; - private List dummyIp; + private List dummyIp; @Before public void setUp() { - dummyIp = new ArrayList(); - dummyIp.add(new IpPortPair("127.0.0.1", 8889)); + dummyIp = new ArrayList(); + dummyIp.add(new HostPortPair("127.0.0.1", 8889)); } @After @@ -89,8 +89,8 @@ int excptnsThrown = 0; int excptnsExpected = 2; launcher = new WebServiceLauncher(); - List ips = new ArrayList<>(); - ips.add(new IpPortPair("127.0.0.1", -10)); + List ips = new ArrayList<>(); + ips.add(new HostPortPair("127.0.0.1", -10)); try { launcher.setIpAddresses(ips); launcher.start(); @@ -98,7 +98,7 @@ excptnsThrown++; } ips = new ArrayList<>(); - ips.add(new IpPortPair("127.0.0.1", 0)); + ips.add(new HostPortPair("127.0.0.1", 0)); try { launcher.setIpAddresses(ips); launcher.start(); diff -r 4c4b627b3f9c -r 4aa310fa7589 web/server/src/main/java/com/redhat/thermostat/web/server/IpPortPair.java --- a/web/server/src/main/java/com/redhat/thermostat/web/server/IpPortPair.java Tue Jan 29 10:55:15 2013 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/* - * Copyright 2012, 2013 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 - * . - * - * 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.web.server; - -public class IpPortPair { - private String ip; - private int port; - - public IpPortPair(String ip, int port) { - this.ip = ip; - this.port = port; - } - - public String getIp() { - return ip; - } - - public int getPort() { - return port; - } -} - diff -r 4c4b627b3f9c -r 4aa310fa7589 web/server/src/main/java/com/redhat/thermostat/web/server/IpPortsParser.java --- a/web/server/src/main/java/com/redhat/thermostat/web/server/IpPortsParser.java Tue Jan 29 10:55:15 2013 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -/* - * Copyright 2012, 2013 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 - * . - * - * 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.web.server; - -import java.util.ArrayList; -import java.util.List; - -/** - * Parses IP/Port pairs from a raw string of the form: - * - * IPv4: - * 127.0.0.1:9999,127.0.0.2:8888 - * - * or - * - * IPv6: - * [1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001 - * - */ -public class IpPortsParser { - - private final String rawString; - private List ipPorts; - private final IllegalArgumentException formatException; - - public IpPortsParser(String parseString) { - this.rawString = parseString; - this.formatException = new IllegalArgumentException("Invalid format of IP/port argument " + rawString); - } - - public void parse() throws IllegalArgumentException { - ipPorts = new ArrayList<>(); - for (String ipPortPair: rawString.split(",")) { - // if we have a '[' in the ip:port pair string we likely have an IPv6 - int idxRparen = ipPortPair.indexOf(']'); - int idxLParen = ipPortPair.indexOf('['); - if (idxLParen == -1) { - // IPv4 - if (idxRparen != -1 || ipPortPair.indexOf(':') == -1) { - throw formatException; - } - String[] ipPort = ipPortPair.split(":"); - int port = -1; - try { - port = Integer.parseInt(ipPort[1]); - } catch (NumberFormatException e) { - throw formatException; - } - ipPorts.add(new IpPortPair(ipPort[0], port)); - } else { - // IPv6 - if (idxRparen == -1) { - throw formatException; - } - int port = -1; - try { - port = Integer.parseInt(ipPortPair.substring(idxRparen + 2)); - } catch (NumberFormatException e) { - throw formatException; - } - ipPorts.add(new IpPortPair(ipPortPair.substring(idxLParen + 1, idxRparen), port)); - } - } - } - - public List getIpsPorts() { - if (ipPorts == null) { - throw new IllegalStateException("Must call parse() before getting map!"); - } - return ipPorts; - } -} - diff -r 4c4b627b3f9c -r 4aa310fa7589 web/server/src/test/java/com/redhat/thermostat/web/server/internal/IpPortsParserTest.java --- a/web/server/src/test/java/com/redhat/thermostat/web/server/internal/IpPortsParserTest.java Tue Jan 29 10:55:15 2013 -0500 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -/* - * Copyright 2012, 2013 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 - * . - * - * 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.web.server.internal; - -import static org.junit.Assert.assertEquals; - -import java.util.List; - -import org.junit.Test; - -import com.redhat.thermostat.web.server.IpPortPair; -import com.redhat.thermostat.web.server.IpPortsParser; - -public class IpPortsParserTest { - - @Test - public void canParsIpV4Pair() throws IllegalArgumentException { - IpPortsParser parser = new IpPortsParser( - "127.0.0.1:8080,127.0.0.1:9999"); - parser.parse(); - List ipPorts = parser.getIpsPorts(); - assertEquals(2, ipPorts.size()); - assertEquals(8080, (long) ipPorts.get(0).getPort()); - assertEquals("127.0.0.1", ipPorts.get(0).getIp()); - assertEquals(9999, (long) ipPorts.get(1).getPort()); - assertEquals("127.0.0.1", ipPorts.get(1).getIp()); - } - - @Test - public void canParseIpv6Pair() { - IpPortsParser parser = new IpPortsParser( - "[1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001"); - parser.parse(); - List ipPorts = parser.getIpsPorts(); - assertEquals(2, ipPorts.size()); - assertEquals(8001, (long) ipPorts.get(0).getPort()); - assertEquals("1fff:0:a88:85a3::ac1f", ipPorts.get(0).getIp()); - assertEquals(8001, (long) ipPorts.get(1).getPort()); - assertEquals("1fff:0:a88:85a3::ac2f", ipPorts.get(1).getIp()); - } - - @Test - public void failsParsingInvalidString() { - IpPortsParser parser = new IpPortsParser( - "1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001"); - int expectedExcptns = 3; - int exptns = 0; - try { - parser.parse(); - } catch (IllegalArgumentException e) { - exptns++; - } - parser = new IpPortsParser("blah,test"); - try { - parser.parse(); - } catch (IllegalArgumentException e) { - exptns++; - } - parser = new IpPortsParser("127.0.0.1:80,127.0.0.2:bad"); - try { - parser.parse(); - } catch (IllegalArgumentException e) { - exptns++; - } - assertEquals(expectedExcptns, exptns); - } - - @Test(expected = IllegalStateException.class) - public void getMapWithNoParseThrowsException() { - IpPortsParser parser = new IpPortsParser("blah"); - parser.getIpsPorts(); - } -} -