Repository: docker-java/docker-java
Pull request #116 (2014-12-19 13:42:21)

Allow SSL configuration from pre-existing keystore to be used
Rather than assuming that the relevant keys are present in the local filesystem.



Patch:
diff --git a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java
index ff1329c6..db27940a 100644
--- a/src/main/java/com/github/dockerjava/core/DockerClientConfig.java
+++ b/src/main/java/com/github/dockerjava/core/DockerClientConfig.java
@@ -6,11 +6,12 @@ import com.google.common.collect.ImmutableMap;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.Serializable;
 import java.net.URI;
 import java.util.Map;
 import java.util.Properties;
 
-public class DockerClientConfig {
+public class DockerClientConfig implements Serializable {
     private static final String DOCKER_HOST_PROPERTY = "DOCKER_HOST";
     private static final String DOCKER_CERT_PATH_PROPERTY = "DOCKER_CERT_PATH";
     private static final String DOCKER_IO_URL_PROPERTY = "docker.io.url";
@@ -41,21 +42,22 @@ public class DockerClientConfig {
             .build();
     private static final String DOCKER_IO_PROPERTIES_PROPERTY = "docker.io.properties";
     private final URI uri;
-    private final String version, username, password, email, serverAddress, dockerCertPath, dockerCfgPath;
+    private final String version, username, password, email, serverAddress, dockerCfgPath;
     private final Integer readTimeout;
     private final boolean loggingFilterEnabled;
+    private final SSLConfig sslConfig;
 
-    DockerClientConfig(URI uri, String version, String username, String password, String email, String serverAddress, String dockerCertPath, String dockerCfgPath, Integer readTimeout, boolean loggingFilterEnabled) {
+    DockerClientConfig(URI uri, String version, String username, String password, String email, String serverAddress, String dockerCfgPath, Integer readTimeout, boolean loggingFilterEnabled, SSLConfig sslConfig) {
         this.uri = uri;
         this.version = version;
         this.username = username;
         this.password = password;
         this.email = email;
         this.serverAddress = serverAddress;
-        this.dockerCertPath = dockerCertPath;
         this.dockerCfgPath = dockerCfgPath;
         this.readTimeout = readTimeout;
         this.loggingFilterEnabled = loggingFilterEnabled;
+        this.sslConfig = sslConfig;
     }
 
     private static Properties loadIncludedDockerProperties(Properties systemProperties) {
@@ -212,15 +214,15 @@ public class DockerClientConfig {
         return loggingFilterEnabled;
     }
 
-    public String getDockerCertPath() {
-        return dockerCertPath;
+    public SSLConfig getSslConfig() {
+      return sslConfig;
     }
 
     public String getDockerCfgPath() {
         return dockerCfgPath;
     }
 
-    @Override
+  @Override
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
@@ -228,7 +230,7 @@ public class DockerClientConfig {
         DockerClientConfig that = (DockerClientConfig) o;
 
         if (loggingFilterEnabled != that.loggingFilterEnabled) return false;
-        if (dockerCertPath != null ? !dockerCertPath.equals(that.dockerCertPath) : that.dockerCertPath != null)
+        if (sslConfig != null ? !sslConfig.equals(that.sslConfig) : that.sslConfig != null)
             return false;
         if (dockerCfgPath != null ? !dockerCfgPath.equals(that.dockerCfgPath) : that.dockerCfgPath != null)
             return false;
@@ -252,8 +254,8 @@ public class DockerClientConfig {
         result = 31 * result + (password != null ? password.hashCode() : 0);
         result = 31 * result + (email != null ? email.hashCode() : 0);
         result = 31 * result + (serverAddress != null ? serverAddress.hashCode() : 0);
-        result = 31 * result + (dockerCertPath != null ? dockerCertPath.hashCode() : 0);
         result = 31 * result + (dockerCfgPath != null ? dockerCfgPath.hashCode() : 0);
+        result = 31 * result + (sslConfig != null ? sslConfig.hashCode() : 0);
         result = 31 * result + (readTimeout != null ? readTimeout.hashCode() : 0);
         result = 31 * result + (loggingFilterEnabled ? 1 : 0);
         return result;
@@ -268,8 +270,8 @@ public class DockerClientConfig {
                 ", password='" + password + '\'' +
                 ", email='" + email + '\'' +
                 ", serverAddress='" + serverAddress + '\'' +
-                ", dockerCertPath='" + dockerCertPath + '\'' +
                 ", dockerCfgPath='" + dockerCfgPath + '\'' +
+                ", sslConfig='" + sslConfig + '\'' +
                 ", readTimeout=" + readTimeout +
                 ", loggingFilterEnabled=" + loggingFilterEnabled +
                 '}';
@@ -277,9 +279,10 @@ public class DockerClientConfig {
 
     public static class DockerClientConfigBuilder {
         private URI uri;
-        private String version, username, password, email, serverAddress, dockerCertPath, dockerCfgPath;
+        private String version, username, password, email, serverAddress, dockerCfgPath;
         private Integer readTimeout;
         private boolean loggingFilterEnabled;
+        private SSLConfig sslConfig;
 
         /**
          * This will set all fields in the builder to those contained in the Properties object. The Properties object
@@ -342,7 +345,7 @@ public class DockerClientConfig {
         }
 
         public final DockerClientConfigBuilder withDockerCertPath(String dockerCertPath) {
-            this.dockerCertPath = dockerCertPath;
+            this.sslConfig = new LocalDirectorySSLConfig(dockerCertPath);
             return this;
         }
 
@@ -352,6 +355,11 @@ public class DockerClientConfig {
         }
 
 
+        public final DockerClientConfigBuilder withSSLConfig(SSLConfig config) {
+            this.sslConfig = config;
+            return this;
+        }
+
         public DockerClientConfig build() {
             return new DockerClientConfig(
                     uri,
@@ -360,10 +368,10 @@ public class DockerClientConfig {
                     password,
                     email,
                     serverAddress,
-                    dockerCertPath,
                     dockerCfgPath,
                     readTimeout,
-                    loggingFilterEnabled
+                    loggingFilterEnabled,
+                    sslConfig
             );
         }
     }
diff --git a/src/main/java/com/github/dockerjava/core/KeystoreSSLConfig.java b/src/main/java/com/github/dockerjava/core/KeystoreSSLConfig.java
new file mode 100644
index 00000000..269b8765
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/KeystoreSSLConfig.java
@@ -0,0 +1,133 @@
+package com.github.dockerjava.core;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * An SSL Config that is based on an pre-existing or pre-loaded KeyStore.
+ */
+public class KeystoreSSLConfig implements SSLConfig, Serializable {
+
+  private final KeyStore keystore;
+  private final String keystorePassword;
+
+  /**
+   * @param keystore a KeyStore
+   * @param keystorePassword key password
+   */
+  public KeystoreSSLConfig(KeyStore keystore, String keystorePassword) {
+    this.keystorePassword = keystorePassword;
+    Preconditions.checkNotNull(keystore);
+    this.keystore = keystore;
+  }
+
+  /**
+   *
+   * @param pfxFile a PKCS12 file
+   * @param password Password for the keystore
+   * @throws KeyStoreException
+   * @throws IOException
+   * @throws CertificateException
+   * @throws NoSuchAlgorithmException
+   */
+  public KeystoreSSLConfig(File pfxFile, String password)
+      throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
+    Preconditions.checkNotNull(pfxFile);
+    Preconditions.checkNotNull(password);
+    keystore = KeyStore.getInstance("pkcs12");
+    keystore.load(new FileInputStream(pfxFile), password.toCharArray());
+    keystorePassword = password;
+  }
+
+
+  /**
+   * Get the SSL Context out of the keystore.
+   * @return java SSLContext
+   * @throws KeyManagementException
+   * @throws UnrecoverableKeyException
+   * @throws NoSuchAlgorithmException
+   * @throws KeyStoreException
+   */
+  @Override
+  public SSLContext getSSLContext()
+      throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException,
+             KeyStoreException {
+
+    final SSLContext context = SSLContext.getInstance("TLS");
+
+    String httpProtocols = System.getProperty("https.protocols");
+    System.setProperty("https.protocols", "TLSv1");
+
+    if (httpProtocols != null)
+      System.setProperty("https.protocols", httpProtocols);
+
+    final KeyManagerFactory
+        keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+    keyManagerFactory.init(keystore, keystorePassword.toCharArray());
+    context.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{
+        new X509TrustManager() {
+          @Override
+          public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[]{};
+          }
+
+          @Override
+          public void checkClientTrusted(final X509Certificate[] arg0, final String arg1) {
+
+          }
+
+          @Override
+          public void checkServerTrusted(final X509Certificate[] arg0, final String arg1) {
+
+          }
+        }
+    }, new SecureRandom());
+
+    return context;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    KeystoreSSLConfig that = (KeystoreSSLConfig) o;
+
+    return keystore.equals(that.keystore);
+
+  }
+
+  @Override
+  public int hashCode() {
+    return keystore.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper(this)
+        .add("keystore", keystore)
+        .toString();
+  }
+}
diff --git a/src/main/java/com/github/dockerjava/core/LocalDirectorySSLConfig.java b/src/main/java/com/github/dockerjava/core/LocalDirectorySSLConfig.java
new file mode 100644
index 00000000..ef57a0de
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/LocalDirectorySSLConfig.java
@@ -0,0 +1,101 @@
+package com.github.dockerjava.core;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+import com.github.dockerjava.api.DockerClientException;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.glassfish.jersey.SslConfigurator;
+
+import java.io.Serializable;
+import java.security.KeyStore;
+import java.security.Security;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * SSL Config from local files.
+ */
+public class LocalDirectorySSLConfig implements SSLConfig, Serializable {
+
+  private final String dockerCertPath;
+
+  public LocalDirectorySSLConfig(String dockerCertPath) {
+    Preconditions.checkNotNull(dockerCertPath);
+    this.dockerCertPath = dockerCertPath;
+  }
+
+  public String getDockerCertPath() {
+    return dockerCertPath;
+  }
+
+  @Override
+  public SSLContext getSSLContext() {
+
+    boolean certificatesExist = CertificateUtils.verifyCertificatesExist(dockerCertPath);
+
+    if (certificatesExist) {
+
+      try {
+
+        Security.addProvider(new BouncyCastleProvider());
+
+        KeyStore keyStore = CertificateUtils.createKeyStore(dockerCertPath);
+        KeyStore trustStore = CertificateUtils.createTrustStore(dockerCertPath);
+
+        // properties acrobatics not needed for java > 1.6
+        String httpProtocols = System.getProperty("https.protocols");
+        System.setProperty("https.protocols", "TLSv1");
+        SslConfigurator sslConfig = SslConfigurator.newInstance(true);
+        if (httpProtocols != null) {
+          System.setProperty("https.protocols", httpProtocols);
+        }
+
+        sslConfig.keyStore(keyStore);
+        sslConfig.keyStorePassword("docker");
+        sslConfig.trustStore(trustStore);
+
+        return sslConfig.createSSLContext();
+
+
+      } catch (Exception e) {
+        throw new DockerClientException(e.getMessage(), e);
+      }
+
+    }
+
+    return null;
+
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    LocalDirectorySSLConfig that = (LocalDirectorySSLConfig) o;
+
+    if (!dockerCertPath.equals(that.dockerCertPath)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return dockerCertPath.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return Objects.toStringHelper(this)
+        .add("dockerCertPath", dockerCertPath)
+        .toString();
+  }
+}
diff --git a/src/main/java/com/github/dockerjava/core/SSLConfig.java b/src/main/java/com/github/dockerjava/core/SSLConfig.java
new file mode 100644
index 00000000..21024c4f
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/SSLConfig.java
@@ -0,0 +1,22 @@
+package com.github.dockerjava.core;
+
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+
+import javax.net.ssl.SSLContext;
+
+/**
+ * Get an SSL Config. Allows for various different implementations.
+ */
+public interface SSLConfig {
+
+  /**
+   * Get the SSL Context, from wherever it comes (file, keystore).
+   * @return an SSL context.
+   */
+  SSLContext getSSLContext()
+      throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException,
+             KeyStoreException;
+}
diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
index 862824fc..64222a97 100644
--- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
+++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
@@ -52,43 +52,16 @@ public class DockerCmdExecFactoryImpl implements DockerCmdExecFactory {
 
         ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig);
 
-        String dockerCertPath = dockerClientConfig.getDockerCertPath();
-
-        if (dockerCertPath != null) {
-            boolean certificatesExist = CertificateUtils.verifyCertificatesExist(dockerCertPath);
-
-            if (certificatesExist) {
-
-                try {
-
-                    Security.addProvider(new BouncyCastleProvider());
-
-                    KeyStore keyStore = CertificateUtils.createKeyStore(dockerCertPath);
-                    KeyStore trustStore = CertificateUtils.createTrustStore(dockerCertPath);
-
-                    // properties acrobatics not needed for java > 1.6
-                    String httpProtocols = System.getProperty("https.protocols");
-                    System.setProperty("https.protocols", "TLSv1");
-                    SslConfigurator sslConfig = SslConfigurator.newInstance(true);
-                    if (httpProtocols != null) System.setProperty("https.protocols", httpProtocols);
-
-                    sslConfig.keyStore(keyStore);
-                    sslConfig.keyStorePassword("docker");
-                    sslConfig.trustStore(trustStore);
-
-                    SSLContext sslContext = sslConfig.createSSLContext();
-
-
-                    clientBuilder.sslContext(sslContext);
-
-                } catch (Exception e) {
-                    throw new DockerClientException(e.getMessage(), e);
-                }
-
-            }
+        try {
+            SSLContext ssl = dockerClientConfig.getSslConfig().getSSLContext();
 
+            if (ssl != null)
+                clientBuilder.sslContext(ssl);
+        } catch(Exception ex) {
+            throw new DockerClientException("Error in SSL Configuration", ex);
         }
 
+
         client = clientBuilder.build();
 
         WebTarget webResource = client.target(dockerClientConfig.getUri());


Test patch:
diff --git a/src/test/java/com/github/dockerjava/core/DockerClientConfigTest.java b/src/test/java/com/github/dockerjava/core/DockerClientConfigTest.java
index 06bb1ea1..7b5e4e1f 100644
--- a/src/test/java/com/github/dockerjava/core/DockerClientConfigTest.java
+++ b/src/test/java/com/github/dockerjava/core/DockerClientConfigTest.java
@@ -16,12 +16,12 @@ public class DockerClientConfigTest {
     public static final DockerClientConfig EXAMPLE_CONFIG = newExampleConfig();
 
     private static DockerClientConfig newExampleConfig() {
-        return new DockerClientConfig(URI.create("http://foo"), "bar", "baz", "qux", "blam", "wham", "flim", "flam", 877, false);
+        return new DockerClientConfig(URI.create("http://foo"), "bar", "baz", "qux", "blam", "wham", "flam", 877, false, new LocalDirectorySSLConfig("flim"));
     }
 
     @Test
     public void string() throws Exception {
-        assertEquals("DockerClientConfig{uri=http://foo, version='bar', username='baz', password='qux', email='blam', serverAddress='wham', dockerCertPath='flim', dockerCfgPath='flam', readTimeout=877, loggingFilterEnabled=false}",
+        assertEquals("DockerClientConfig{uri=http://foo, version='bar', username='baz', password='qux', email='blam', serverAddress='wham', dockerCfgPath='flam', sslConfig='LocalDirectorySSLConfig{dockerCertPath=flim}', readTimeout=877, loggingFilterEnabled=false}",
                 EXAMPLE_CONFIG.toString());
     }
 
@@ -104,8 +104,8 @@ public class DockerClientConfigTest {
         assertEquals(config.getServerAddress(), AuthConfig.DEFAULT_SERVER_ADDRESS);
         assertEquals(config.getVersion(), null);
         assertEquals(config.isLoggingFilterEnabled(), true);
-        assertEquals(config.getDockerCertPath(), "someHomeDir/.docker");
         assertEquals(config.getDockerCfgPath(), "someHomeDir/.dockercfg");
+        assertEquals( ((LocalDirectorySSLConfig)config.getSslConfig()).getDockerCertPath(), "someHomeDir/.docker");
     }
 
     @Test
diff --git a/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java b/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java
index c31db1ba..a91fa109 100644
--- a/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java
+++ b/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java
@@ -10,7 +10,7 @@ public class DockerClientImplTest {
     @Test
     public void configuredInstanceAuthConfig() throws Exception {
         // given a config with null serverAddress
-        DockerClientConfig dockerClientConfig = new DockerClientConfig(null, null, "", "", "", null, null, null, 0, false);
+        DockerClientConfig dockerClientConfig = new DockerClientConfig(null, null, "", "", "", null, null,  0, false, null);
         DockerClientImpl dockerClient = DockerClientImpl.getInstance(dockerClientConfig);
 
         // when we get the auth config