changeset 944:4aa310fa7589

Add mongodb client TLS support. Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-January/005226.html PR1243
author Severin Gehwolf <sgehwolf@redhat.com>
date Thu, 31 Jan 2013 11:53:43 +0100
parents 4c4b627b3f9c
children fb898142c539
files common/core/src/main/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfiguration.java common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortPair.java common/core/src/main/java/com/redhat/thermostat/common/utils/HostPortsParser.java common/core/src/test/java/com/redhat/thermostat/common/ssl/SSLKeystoreConfigurationTest.java common/core/src/test/java/com/redhat/thermostat/common/utils/HostPortsParserTest.java common/core/src/test/resources/client.properties distribution/config/db.properties distribution/config/ssl.properties storage/core/pom.xml storage/mongo/pom.xml storage/mongo/src/main/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnection.java storage/mongo/src/test/java/com/redhat/thermostat/storage/mongodb/internal/MongoConnectionTest.java web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceCommand.java web/cmd/src/main/java/com/redhat/thermostat/web/cmd/WebServiceLauncher.java web/cmd/src/test/java/com/redhat/thermostat/web/cmd/WebServiceLauncherTest.java web/server/src/main/java/com/redhat/thermostat/web/server/IpPortPair.java web/server/src/main/java/com/redhat/thermostat/web/server/IpPortsParser.java web/server/src/test/java/com/redhat/thermostat/web/server/internal/IpPortsParserTest.java
diffstat 18 files changed, 527 insertions(+), 303 deletions(-) [+]
line wrap: on
line diff
--- 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) {
--- /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
+ * <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.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;
+    }
+}
+
--- /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
+ * <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.common.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses Host/Port pairs from a raw string.
+ * 
+ * <pre>
+ * IPv4:
+ *      127.0.0.1:9999,127.0.0.2:8888
+ * </pre>
+ * 
+ * or
+ * 
+ * <pre>
+ * IPv6:
+ *      [1fff:0:a88:85a3::ac1f]:8001,[1fff:0:a88:85a3::ac2f]:8001
+ * </pre>
+ * 
+ * or
+ * 
+ * <pre>
+ * DNS hostname:port pairs:
+ *      testhost.example.com:8970,host2.example.com:1234
+ * </pre>
+ * 
+ * Be sure to call <code>{@link #parse()}</code> before getting the list of
+ * host/port pairs via <code>{@link #getHostsPorts()}</code>.
+ */
+public class HostPortsParser {
+
+    private final String rawString;
+    private List<HostPortPair> 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<HostPortPair> getHostsPorts() {
+        if (ipPorts == null) {
+            throw new IllegalStateException("Must call parse() before getting map!");
+        }
+        return ipPorts;
+    }
+}
+
--- 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());
     }
 }
 
--- /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
+ * <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.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<HostPortPair> 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<HostPortPair> 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<HostPortPair> 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();
+    }
+}
+
--- 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
--- 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
-
--- 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
--- 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 @@
       <version>${project.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.redhat.thermostat</groupId>
+      <artifactId>thermostat-common-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
 </project>
--- 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 @@
     	<artifactId>thermostat-storage-core</artifactId>
     	<version>${project.version}</version>
     </dependency>
+    <dependency>
+        <groupId>com.redhat.thermostat</groupId>
+        <artifactId>thermostat-common-core</artifactId>
+        <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
 </project>
--- 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;
+    }
 }
 
--- 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<MongoOptions> 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
+        }
+    }
 }
 
--- 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<IpPortPair> parseIPsPorts(String rawIpsPorts) throws CommandException {
-        IpPortsParser parser = new IpPortsParser(rawIpsPorts);
+    private List<HostPortPair> 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();
     }
 
 }
--- 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<IpPortPair> ipsPorts;
+    private List<HostPortPair> 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<IpPortPair> ipsPorts) {
+    public void setIpAddresses(List<HostPortPair> 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());
             }
--- 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<IpPortPair> dummyIp;
+    private List<HostPortPair> dummyIp;
     
     @Before
     public void setUp() {
-        dummyIp = new ArrayList<IpPortPair>();
-        dummyIp.add(new IpPortPair("127.0.0.1", 8889));
+        dummyIp = new ArrayList<HostPortPair>();
+        dummyIp.add(new HostPortPair("127.0.0.1", 8889));
     }
     
     @After
@@ -89,8 +89,8 @@
         int excptnsThrown = 0;
         int excptnsExpected = 2;
         launcher = new WebServiceLauncher();
-        List<IpPortPair> ips = new ArrayList<>();
-        ips.add(new IpPortPair("127.0.0.1", -10));
+        List<HostPortPair> 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();
--- 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
- * <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.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;
-    }
-}
-
--- 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
- * <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.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<IpPortPair> 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<IpPortPair> getIpsPorts() {
-        if (ipPorts == null) {
-            throw new IllegalStateException("Must call parse() before getting map!");
-        }
-        return ipPorts;
-    }
-}
-
--- 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
- * <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.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<IpPortPair> 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<IpPortPair> 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();
-    }
-}
-