diff --git a/Readme.md b/Readme.md index 10c9a78d..9043e949 100644 --- a/Readme.md +++ b/Readme.md @@ -65,6 +65,12 @@ More example requests: ;; Need to contact a server with an untrusted SSL cert? (client/get "https://alioth.debian.org" {:insecure? true}) +;; Need to specify a trust store? +(client/get "https://my.corp.com" {:trust-store "/path/to/trust-store.jks" + :trust-store-type "jks" ; default jks + :trust-store-pass "trustpass" + :security-protocol "TLS" ; default TLS}) + ;; If you don't want to follow-redirects automatically: (client/get "http://site.come/redirects-somewhere" {:follow-redirects false}) diff --git a/src/clj_http/lite/core.clj b/src/clj_http/lite/core.clj index 246d1d2f..183f7011 100644 --- a/src/clj_http/lite/core.clj +++ b/src/clj_http/lite/core.clj @@ -1,8 +1,10 @@ (ns clj-http.lite.core "Core HTTP request/response implementation." (:require [clojure.java.io :as io]) - (:import (java.io ByteArrayOutputStream InputStream IOException) - (java.net URI URL HttpURLConnection))) + (:import [java.io ByteArrayOutputStream InputStream IOException] + [java.net URI URL HttpURLConnection] + [javax.net.ssl HttpsURLConnection SSLContext TrustManagerFactory] + [java.security KeyStore])) (defn parse-headers "Takes a URLConnection and returns a map of names to values. @@ -39,6 +41,25 @@ (.flush baos) (.toByteArray baos))))) +(defn get-connection [url] + (.openConnection ^URL (URL. url))) + +(defn set-trust-store + [^HttpsURLConnection conn + {:keys [trust-store trust-store-pass trust-store-type security-protocol] + :or {trust-store-type "jks" security-protocol "TLS"}}] + (let [ssl-context (SSLContext/getInstance security-protocol) + key-store (KeyStore/getInstance trust-store-type) + trust-manager-factory (TrustManagerFactory/getInstance "SunX509")] + (.load key-store (io/input-stream + (or (io/resource trust-store) + (io/file trust-store))) + (char-array trust-store-pass)) + (.init trust-manager-factory key-store) + (.init ssl-context nil (.getTrustManagers trust-manager-factory) nil) + (.setSSLSocketFactory conn (.getSocketFactory ssl-context)) + conn)) + (defn request "Executes the HTTP request corresponding to the given Ring request map and returns the Ring response map corresponding to the resulting HTTP response. @@ -48,12 +69,15 @@ [{:keys [request-method scheme server-name server-port uri query-string headers content-type character-encoding body socket-timeout conn-timeout multipart debug insecure? save-request? follow-redirects - chunk-size] :as req}] + chunk-size trust-store trust-store-pass] :as req}] (let [http-url (str (name scheme) "://" server-name (when server-port (str ":" server-port)) uri (when query-string (str "?" query-string))) - conn (.openConnection ^URL (URL. http-url))] + ^HttpURLConnection conn + (cond-> (get-connection http-url) + (and (= scheme :https) trust-store trust-store-pass) + (set-trust-store req))] (when (and content-type character-encoding) (.setRequestProperty conn "Content-Type" (str content-type "; charset=" @@ -63,8 +87,8 @@ (doseq [[h v] headers] (.setRequestProperty conn h v)) (when (false? follow-redirects) - (.setInstanceFollowRedirects ^HttpURLConnection conn false)) - (.setRequestMethod ^HttpURLConnection conn (.toUpperCase (name request-method))) + (.setInstanceFollowRedirects conn false)) + (.setRequestMethod conn (.toUpperCase (name request-method))) (when body (.setDoOutput conn true)) (when socket-timeout @@ -78,9 +102,9 @@ (with-open [out (.getOutputStream conn)] (io/copy body out))) (merge {:headers (parse-headers conn) - :status (.getResponseCode ^HttpURLConnection conn) + :status (.getResponseCode conn) :body (when-not (= request-method :head) (coerce-body-entity req conn))} (when save-request? {:request (assoc (dissoc req :save-request?) - :http-url http-url)})))) + :http-url http-url)})))) diff --git a/test-resources/README.org b/test-resources/README.org new file mode 100644 index 00000000..5fc9d063 --- /dev/null +++ b/test-resources/README.org @@ -0,0 +1,12 @@ +* Generate CA and import into keystore and truststore +openssl req -new -x509 -keyout ca-key.pem -out ca-cert.pem -days 3650 +keytool -import -keystore truststore.jks -file ca-cert.pem +keytool -import -keystore keystore.jks -file ca-cert.pem + +* Generate key and CSR for server (Jetty running on localhost) +keytool -keystore keystore.jks -genkey -alias localhost +keytool -keystore keystore.jks -certreq -alias localhost -keyalg rsa -file localhost.csr + +* Sign CSR with CA cert and import into keystore +openssl x509 -req -CA ca-cert.pem -CAkey ca-key.pem -in localhost.csr -out localhost.cer -days 3650 -CAcreateserial +keytool -import -keystore keystore.jks -file localhost.cer -alias localhost diff --git a/test-resources/ca-cert.pem b/test-resources/ca-cert.pem new file mode 100644 index 00000000..30561159 --- /dev/null +++ b/test-resources/ca-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiDCCAnCgAwIBAgIJANa5pPKrnuBoMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xODA2MTQyMjM4 +MjRaFw0yODA2MTEyMjM4MjRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV +BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMtf +R79GuNTxPZcISS/iSSC0ejkUAhSysQb0a3zSvKCGZmxFtV0LtgIQFS/KlyTIKokr +BIV5ToOB6Zy7jc7bKoCaPvEPjT8i8rnjCUtFb+XFITdi0TI+macYb2/CCgrY7ePG +aewO+kK20eVeSjygPgi8svEsdVyIpH8PEuAxCdjsCo8F3EHQHhrokEqUh+rLA9AX +A0uhJAkiPUGdxozE5L68WrvUs8GD9I8QmCYBldhvjD5nI5NFVDF8yZduWX6NuX5g +1etxZ86ob1Tcy4BtVVYskCjHuT57mMFdspsC59cUiM1RUYOia/GkgDK38An2NJyN +OEr3MC77m1qP0XOvJ/0CAwEAAaNTMFEwHQYDVR0OBBYEFAvylOxjTNSDjCqBlncO +HBaDc5L5MB8GA1UdIwQYMBaAFAvylOxjTNSDjCqBlncOHBaDc5L5MA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFCVTNyT4RR64lEe03y487s8efyp +HX9kxbS2PyUx327PakqVhu/6EU1VWEI8FIwxFYQB+WpTjh5qB/8y1Y4TMWAD7VRH +MsVzf3k4Wf+jH0GaO9ZNI4NSXXlZHKjxCb+ahQOcnEFXRog6f1locJIcJlNZ6JEJ +Tncnehe5YH0YhBhRZuR46ACExMSjfF+6rymxJnQg1KkGQaA7ypu79PF9wcc/tzfR +RFMP8esEQARWDYQRuA9Ms2DDDYE5lZx0QYjh/ljnfNxmimmRtg7TFkpadkaAChHj +jEaoDHPe5y4ktrK6N5snKDX0XhLLTQptntYUOugT/NCylqaATRUQVTQNCSY= +-----END CERTIFICATE----- diff --git a/test-resources/ca-cert.srl b/test-resources/ca-cert.srl new file mode 100644 index 00000000..e629314c --- /dev/null +++ b/test-resources/ca-cert.srl @@ -0,0 +1 @@ +E9DE7EDC3590114D diff --git a/test-resources/ca-key.pem b/test-resources/ca-key.pem new file mode 100644 index 00000000..b84745d9 --- /dev/null +++ b/test-resources/ca-key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQILkQiXuciC7QCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECBYw8AvZ5P56BIIEyB+Uh5zShr1o +fSL1oMlKOizKDkfNYNc2k/lV4I/X/vCARSAKa5/bU3dAh82p16Te3Z1N99X7LWTk +Bd2WrcIjZxm8fC20ew/zD4/fVKOI/t56WUV6tavNyEOfLhzgrtWv7eo5an65V1QW +eYS9syDCaHocQZzI96m+dyDKy7cJMUaVJIIlasGEuV0Lt2pkg6WfMHJbmd5wyqyM +/tsx8aIkZlDVvXdddI+OfQ7Pa0LlTqXfuYqmEuez4Uz6uP6EaRnOyXlbWhtJeMAG +NaRQ95Ew339k+4j2N/o45DLWXORiBZxCD0YmyioZiFEvA4BnzzJ/R2BtY7v3zj+X +S7puFG21Lwe2zb1PQoKljVeJVWwbCaOzL3E+oXHj7xz26OxYhGT6xn0YT6HJaOjE +1Np9Kv+UeZi+zw2C8s7ru1b0UaIoRpwNH4+d39N8lBEaBlW2byZ+hV51BwbnLVOs +t5G2P8ppKsg8hSlxAw6hgyiTqXvvo4BX1U9aBvznt8dUI35dzhBG4G0u3UrYLEw0 +eYMecbSR12xQ1kRf+gO+g1uxsMY5odnz9I0WhwVGzEyjKdAXdQf/xZOtb+A2bUZx +RO/Ir1qSr58A1kUOyTWhv4EWxhviStqc9P0jQlA0olkkkZ2OULlXKh49Qemukbms +Og6jHIAljsbh4hZfyKFSUJJGC3GftoaMYErLcGJw3BmEavZcFM1z4FWu6HoQ5nEP +aDtGf0/ZN653VE0YRlmjxL85o7V3ZkOGakdTZnNaFi8wEv0nMCq+DbN5LghWokyM +bSdus2hmZr2j7fScYvdh+TBAOO73+ziVVM34SHqmcUYuuH4vv94GJwYLo3LUbDLL +uIQFJmBXekVpZ3l3HqSge5JYMXQ5vM11cJpjFrnyCNDISnunGpfDc2TGn4/CRQ4v +1EjwZLoWiQBsiw3FnKCqSLtl6pHcyMgWS/wkeQqHOcOyK4bFKBhtfe/Yp/xsTLt5 +6n7Yo0VGe6qcRTA5H+fkMRzVlFmrmqB6eN9Md85YQ7bK6nfOg2jbUMejHJUTxPC3 +BhRq0rMQXDhHvdomzxijWSznwtZs31UiG3Zis52ua4/Ux3K1yTKXVbfWqBhMNuVW +r7pvsTuLh+cdZQe4dxQgZXTNdVqrKa3yE0h6zbhBPlV1PXGzbSw8RSK0wYhJr167 +MPN7S74YJTjV2ujhLD4cUEONACXbLI2FE3jx2lLKbFN/Ih421x1lJWtADWnT6CDB +19/kmNyI2bCkYGomoQ9qaVw7p7a0d9OV5VRXavEn7scAhOj4/cNrAukvedionpIx +autWzyZAyAOpmGCTtlRnImDBfs8tpWqf36qfAxIM3gZlKGB2cijNAKbYEld9XzFX +Kw0CWvZro3/QOkqabtcwvcoupxX2mZsju5uahlJwZ1xQFz4j6svtYwvpMRpySI+K +mzpRcDhNsY9vY3lKHHYw8ModxIZ1WlHz5xf9FsrQEsKpseZXy7ItxtIimu3saufj +6XHvQl4K88/W4d2Q+WoUIkhK1sU4mcad3WXjxhOEgboNqsV41U5QJTAYOTecdfTp +PcrHf/EFwwiePRruw90DiGB+fCqBGl5B3bpUORl0wjpSXHfxW3RBpA0KkXByeB4C +wm31ey9mnr8ylJ1p6chHog== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test-resources/keystore b/test-resources/keystore deleted file mode 100644 index 24b84f38..00000000 Binary files a/test-resources/keystore and /dev/null differ diff --git a/test-resources/keystore.jks b/test-resources/keystore.jks new file mode 100644 index 00000000..053df123 Binary files /dev/null and b/test-resources/keystore.jks differ diff --git a/test-resources/localhost.cer b/test-resources/localhost.cer new file mode 100644 index 00000000..0f772f73 --- /dev/null +++ b/test-resources/localhost.cer @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFYzCCBEsCCQDp3n7cNZARTTANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwNjE0MjI1NjIwWhcN +MjgwNjExMjI1NjIwWjBuMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtu +b3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYDVQQL +EwdVbmtub3duMRIwEAYDVQQDEwlsb2NhbGhvc3QwggNCMIICNQYHKoZIzjgEATCC +AigCggEBAI95Ndm5qum/q+2Ies9JUbbzLsWeO683GOjqxJYfPv02BudDUanEGDM5 +uAnnwq4cU5unR1uF0BGtuLR5h3VJhGlcrA6PFLM2CCiiL/onEQo9YqmTRTQJoP5p +bEZY+EvdIIGcNwmgEFexla3NACM9ulSEtikfnWSO+INEhneXnOwEtDSmrC516Zhd +4j2wKS/BEYyf+p2BgeczjbeStzDXueNJWS9oCZhyFTkV6j1ri0ZTxjNFj4A7MqTC +4PJykCVuTj+KOwg4ocRQ5OGMGimjfd9eoUPeS2b/BJA+1c8WI+FY1IfGCOl/IRzY +Hcojy244B2X4IuNCvkhMBXY5OWAc1mcCHQC69pamhXj3397n+mfJd8eF7zKyM7rl +gMC81WldAoIBABamXFggSFBwTnUCo5dXBA002jo0eMFU1OSlwC0kLuBPluYeS9CQ +Sr2sjzfuseCfMYLSPJBDy2QviABBYO35ygmzIHannDKmJ/JHPpGHm6LE50S9IIFU +TLVbgCw2jR+oPtSJ6U4PoGiOMkKKXHjEeMaNBSe3HJo6uwsL4SxEaJY559POdNsQ +GmWqK4f2TGgm2z7HL0tVmYNLtO2wL3yQ6aSW06VdU1vr/EXU9hn2Pz3tu4c5JcLy +JOB3MSltqIfsHkdI+H77X963VIQxayIy3uVT3a8CESsNHwLaMJcyJP4nrtqLnUsp +Itm6i+Oe2eEDpjxSgQvGiLfi7UMW4e8X294DggEFAAKCAQBj5iGy71+T7dyNUGYR +4tcboG43+q3i96dH+ft7opHNDBqPhAUo++YukC9Q9leMmyqjd8be5VGkPGxjePuS +wN6mQK2ilzAyTGUL2ICJdKE7U0b8AMy42rjoZVGwkRkpfahUkgfEB10TtS+AlbIZ +1azN98V+7yXwYPe1J5ioU9jDGMNC717ySBjORHIlOVmGOLK4Uylo2gW03he1nkAb +BxXoJATgV3VqwddJb5DMJoB0o5jwyMzL/cZHSABhukdrgK/KdAonVy0wzDQyScIj +3NIRCiDhFFGu0ZsruSG/vfNl1QLHToPzhgGIuGDO471hRP7Y1b/CE7eIzViyreTZ +LZW3MA0GCSqGSIb3DQEBCwUAA4IBAQB5cyUQA4maQogpheAwIsHD1R7TQFApu79N +1fKlvGGYutVo0EYv+FvIZsVmkoKKF725xXfjCfFOwIlGF6OrK/3xJNWBrSZr/jHs +1hOzdP0/Kmp63U2jfhBqn/7PiWYSvLUnwx7hj2qKhoGXhrXGTEs2kSyiRt92UFLM +9u+sajB2Wn0P3lxFHFI4Djv5uq4/Gb/Qjt2hJsB3W5yUdTgdpqyghDzrblXhrkhp +xQJhMb2pVLI7ZaJgBst0Ymllf4PHry1PF7WcgG0RlsTR8Xjh9YFcmnSAw2Hp/eDY +lsZBbkdZbjgqOifTGqBJ5RbTL3QFR3UFw3lMkTxDXYHzvwJ+iVhd +-----END CERTIFICATE----- diff --git a/test-resources/localhost.csr b/test-resources/localhost.csr new file mode 100644 index 00000000..e1f00dcf --- /dev/null +++ b/test-resources/localhost.csr @@ -0,0 +1,25 @@ +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIEQDCCA+sCAQAwbjEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93 +bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMH +VW5rbm93bjESMBAGA1UEAxMJbG9jYWxob3N0MIIDQjCCAjUGByqGSM44BAEwggIo +AoIBAQCPeTXZuarpv6vtiHrPSVG28y7FnjuvNxjo6sSWHz79NgbnQ1GpxBgzObgJ +58KuHFObp0dbhdARrbi0eYd1SYRpXKwOjxSzNggooi/6JxEKPWKpk0U0CaD+aWxG +WPhL3SCBnDcJoBBXsZWtzQAjPbpUhLYpH51kjviDRIZ3l5zsBLQ0pqwudemYXeI9 +sCkvwRGMn/qdgYHnM423krcw17njSVkvaAmYchU5Feo9a4tGU8YzRY+AOzKkwuDy +cpAlbk4/ijsIOKHEUOThjBopo33fXqFD3ktm/wSQPtXPFiPhWNSHxgjpfyEc2B3K +I8tuOAdl+CLjQr5ITAV2OTlgHNZnAh0AuvaWpoV499/e5/pnyXfHhe8ysjO65YDA +vNVpXQKCAQAWplxYIEhQcE51AqOXVwQNNNo6NHjBVNTkpcAtJC7gT5bmHkvQkEq9 +rI837rHgnzGC0jyQQ8tkL4gAQWDt+coJsyB2p5wypifyRz6Rh5uixOdEvSCBVEy1 +W4AsNo0fqD7UielOD6BojjJCilx4xHjGjQUntxyaOrsLC+EsRGiWOefTznTbEBpl +qiuH9kxoJts+xy9LVZmDS7TtsC98kOmkltOlXVNb6/xF1PYZ9j897buHOSXC8iTg +dzEpbaiH7B5HSPh++1/et1SEMWsiMt7lU92vAhErDR8C2jCXMiT+J67ai51LKSLZ +uovjntnhA6Y8UoELxoi34u1DFuHvF9veA4IBBQACggEAY+Yhsu9fk+3cjVBmEeLX +G6BuN/qt4venR/n7e6KRzQwaj4QFKPvmLpAvUPZXjJsqo3fG3uVRpDxsY3j7ksDe +pkCtopcwMkxlC9iAiXShO1NG/ADMuNq46GVRsJEZKX2oVJIHxAddE7UvgJWyGdWs +zffFfu8l8GD3tSeYqFPYwxjDQu9e8kgYzkRyJTlZhjiyuFMpaNoFtN4XtZ5AGwcV +6CQE4Fd1asHXSW+QzCaAdKOY8MjMy/3GR0gAYbpHa4CvynQKJ1ctMMw0MknCI9zS +EQog4RRRrtGbK7khv73zZdUCx06D84YBiLhgzuO9YUT+2NW/whO3iM1Ysq3k2S2V +t6AwMC4GCSqGSIb3DQEJDjEhMB8wHQYDVR0OBBYEFP+8yPg7WQpeC6R7LxYkW/FW +C21UMA0GCWCGSAFlAwQDAgUAA0AAMD0CHBBBmwajAf0D5p7cRxAj4iq/DUTMOyBu +JZBDhcQCHQCcd1nksVdQIHO7f27vJ4u0nC/1gm6IBuwhnocn +-----END NEW CERTIFICATE REQUEST----- diff --git a/test-resources/truststore.jks b/test-resources/truststore.jks new file mode 100644 index 00000000..34cdefc3 Binary files /dev/null and b/test-resources/truststore.jks differ diff --git a/test/clj_http/test/core.clj b/test/clj_http/test/core.clj index 66bb778f..8948fc67 100644 --- a/test/clj_http/test/core.clj +++ b/test/clj_http/test/core.clj @@ -138,19 +138,35 @@ (deftest ^{:integration true} self-signed-ssl-get (let [t (doto (Thread. #(ring/run-jetty handler {:port 8081 :ssl-port 18082 :ssl? true - :keystore "test-resources/keystore" - :key-password "keykey"})) .start)] + :keystore "test-resources/keystore.jks" + :key-password "changeit"})) .start)] (Thread/sleep 1000) (try (is (thrown? javax.net.ssl.SSLException (request {:request-method :get :uri "/get" :server-port 18082 :scheme :https}))) #_(let [resp (request {:request-method :get :uri "/get" :server-port 18082 - :scheme :https :insecure? true})] - (is (= 200 (:status resp))) - (is (= "get" (slurp-body resp)))) + :scheme :https :insecure? true})] + (is (= 200 (:status resp))) + (is (= "get" (slurp-body resp)))) (finally - (.stop t))))) + (.stop t))))) + +(deftest ^{:integration true} self-signed-ssl-get-with-trust-store + (let [t (doto (Thread. #(ring/run-jetty handler + {:port 8082 :ssl-port 18083 :ssl? true + :keystore "test-resources/keystore.jks" + :key-password "changeit"})) .start)] + (Thread/sleep 1000) + (try + (let [resp (request {:request-method :get :uri "/get" :server-port 18083 + :scheme :https + :trust-store "test-resources/truststore.jks" + :trust-store-pass "changeit"})] + (is (= 200 (:status resp))) + (is (= "get" (slurp-body resp)))) + (finally + (.stop t))))) ;; (deftest ^{:integration true} multipart-form-uploads ;; (run-server)