diff --git a/project.clj b/project.clj index 1c5f39c..80fd5a9 100644 --- a/project.clj +++ b/project.clj @@ -8,7 +8,7 @@ :dependencies [[org.clojure/clojure "1.5.1"] [http-kit "2.1.16"] - [puppetlabs/certificate-authority "0.1.3"] + [puppetlabs/certificate-authority "0.1.4"] [org.clojure/tools.logging "0.2.6"] [org.slf4j/slf4j-api "1.7.6"]] diff --git a/src/clj/puppetlabs/http/client/async.clj b/src/clj/puppetlabs/http/client/async.clj index c64e69b..3d29d0b 100644 --- a/src/clj/puppetlabs/http/client/async.clj +++ b/src/clj/puppetlabs/http/client/async.clj @@ -28,6 +28,13 @@ (:ssl-ca-cert req))) (dissoc :ssl-cert :ssl-key :ssl-ca-cert))) +(defn- initialize-ssl-context-from-ca-pem + [req] + (-> req + (assoc :ssl-context (ssl/ca-cert-pem->ssl-context + (:ssl-ca-cert req))) + (dissoc :ssl-ca-cert))) + (defn- configure-ssl-from-context "Configures an SSLEngine in the request starting from an SSLContext" [req] @@ -42,6 +49,13 @@ initialize-ssl-context-from-pems configure-ssl-from-context)) +(defn- configure-ssl-from-ca-pem + "Configures an SSLEngine in the request starting from a CA PEM file" + [req] + (-> req + initialize-ssl-context-from-ca-pem + configure-ssl-from-context)) + (defn configure-ssl "Configures a request map to have an SSLEngine. It will use an existing one if already present, , then use an SSLContext (stored in :ssl-context) if @@ -53,6 +67,7 @@ (:sslengine req) req (:ssl-context req) (configure-ssl-from-context req) (every? (partial req) [:ssl-cert :ssl-key :ssl-ca-cert]) (configure-ssl-from-pems req) + (:ssl-ca-cert req) (configure-ssl-from-ca-pem req) :else req)) (defn- check-url! [url] diff --git a/src/java/com/puppetlabs/http/client/HttpClientException.java b/src/java/com/puppetlabs/http/client/HttpClientException.java new file mode 100644 index 0000000..a4cf066 --- /dev/null +++ b/src/java/com/puppetlabs/http/client/HttpClientException.java @@ -0,0 +1,7 @@ +package com.puppetlabs.http.client; + +public class HttpClientException extends RuntimeException { + public HttpClientException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/src/java/com/puppetlabs/http/client/SyncHttpClient.java b/src/java/com/puppetlabs/http/client/SyncHttpClient.java index 69a0b30..575eae8 100644 --- a/src/java/com/puppetlabs/http/client/SyncHttpClient.java +++ b/src/java/com/puppetlabs/http/client/SyncHttpClient.java @@ -20,7 +20,7 @@ public class SyncHttpClient { private static void logAndRethrow(String msg, Throwable t) { LOGGER.error(msg, t); - throw new RuntimeException(msg, t); + throw new HttpClientException(msg, t); } private static RequestOptions configureSslFromContext(RequestOptions options) { @@ -68,6 +68,27 @@ public class SyncHttpClient { return configureSslFromContext(options); } + if (options.getSslCaCert() != null) { + try { + options.setSslContext( + CertificateAuthority.caCertPemToSSLContext( + new FileReader(options.getSslCaCert())) + ); + } catch (KeyStoreException e) { + logAndRethrow("Error while configuring SSL", e); + } catch (CertificateException e) { + logAndRethrow("Error while configuring SSL", e); + } catch (IOException e) { + logAndRethrow("Error while configuring SSL", e); + } catch (NoSuchAlgorithmException e) { + logAndRethrow("Error while configuring SSL", e); + } catch (KeyManagementException e) { + logAndRethrow("Error while configuring SSL", e); + } + options.setSslCaCert(null); + return configureSslFromContext(options); + } + return options; } diff --git a/test/puppetlabs/http/client/async_test.clj b/test/puppetlabs/http/client/async_test.clj index 0e1d4d3..7f23f90 100644 --- a/test/puppetlabs/http/client/async_test.clj +++ b/test/puppetlabs/http/client/async_test.clj @@ -21,6 +21,16 @@ (is (not (:ssl-key configured-req))) (is (not (:ssl-ca-cert configured-req)))))) +(deftest ssl-config-with-ca-file + (let [req {:ssl-ca-cert (resource "ssl/ca.pem")} + configured-req (http/configure-ssl req)] + + (testing "configure-ssl sets up an SSLEngine when given ca-cert" + (is (instance? SSLEngine (:sslengine configured-req)))) + + (testing "removes ssl-ca-cert" + (is (not (:ssl-ca-cert configured-req)))))) + (deftest ssl-config-without-ssl-params (let [req {:url "http://localhost" :method :get} diff --git a/test/puppetlabs/http/client/sync_test.clj b/test/puppetlabs/http/client/sync_test.clj index 48089ce..d67c5f1 100644 --- a/test/puppetlabs/http/client/sync_test.clj +++ b/test/puppetlabs/http/client/sync_test.clj @@ -1,5 +1,7 @@ (ns puppetlabs.http.client.sync-test - (:import (com.puppetlabs.http.client SyncHttpClient RequestOptions)) + (:import (com.puppetlabs.http.client SyncHttpClient RequestOptions + HttpClientException) + (javax.net.ssl SSLHandshakeException)) (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] @@ -18,7 +20,7 @@ (add-ring-handler app "/hello") context)) -(deftest sync-client-test +(deftest sync-client-test-from-pems (testlogging/with-test-logging (testutils/with-app-with-config app [jetty9/jetty9-service test-web-service] @@ -42,3 +44,49 @@ :ssl-ca-cert "./dev-resources/ssl/ca.pem"})] (is (= 200 (:status response))) (is (= "Hello, World!" (slurp (:body response))))))))) + +(deftest sync-client-test-from-ca-cert + (testlogging/with-test-logging + (testutils/with-app-with-config app + [jetty9/jetty9-service test-web-service] + {:webserver {:ssl-host "0.0.0.0" + :ssl-port 10080 + :ssl-ca-cert "./dev-resources/ssl/ca.pem" + :ssl-cert "./dev-resources/ssl/cert.pem" + :ssl-key "./dev-resources/ssl/key.pem" + :client-auth "want"}} + (testing "java sync client" + (let [options (.. (RequestOptions. "https://localhost:10080/hello/") + (setSslCaCert "./dev-resources/ssl/ca.pem")) + response (SyncHttpClient/get options)] + (is (= 200 (.getStatus response))) + (is (= "Hello, World!" (slurp (.getBody response)))))) + (testing "clojure sync client" + (let [response (sync/get "https://localhost:10080/hello/" + {:ssl-ca-cert "./dev-resources/ssl/ca.pem"})] + (is (= 200 (:status response))) + (is (= "Hello, World!" (slurp (:body response))))))))) + +(deftest sync-client-test-with-invalid-ca-cert + (testlogging/with-test-logging + (testutils/with-app-with-config app + [jetty9/jetty9-service test-web-service] + {:webserver {:ssl-host "0.0.0.0" + :ssl-port 10081 + :ssl-ca-cert "./dev-resources/ssl/ca.pem" + :ssl-cert "./dev-resources/ssl/cert.pem" + :ssl-key "./dev-resources/ssl/key.pem" + :client-auth "want"}} + (testing "java sync client" + (let [options (.. (RequestOptions. "https://localhost:10081/hello/") + (setSslCaCert "./dev-resources/ssl/alternate-ca.pem"))] + (try + (SyncHttpClient/get options) + ; fail if we don't get an exception + (is (not true) "expected HttpClientException") + (catch HttpClientException e + (is (instance? SSLHandshakeException (.getCause e))))))) + (testing "clojure sync client" + (is (thrown? SSLHandshakeException + (sync/get "https://localhost:10081/hello/" + {:ssl-ca-cert "./dev-resources/ssl/alternate-ca.pem"})))))))