Skip to content
Permalink
Browse files

Add :cookie-spec and :cookie-policy-registry options

This adds two advanced options for specifying the `CookieSpec` that Apache
should use when parsing/validating cookies.

Relates to #444
  • Loading branch information
dakrone committed Apr 11, 2018
1 parent 1001815 commit 2608c3014cf49a800999c43771a8fccd1bfce363
Showing with 133 additions and 9 deletions.
  1. +53 −4 README.org
  2. +5 −0 changelog.org
  3. +31 −4 src/clj_http/core.clj
  4. +44 −1 test/clj_http/test/core_test.clj
@@ -195,10 +195,59 @@ Example requests:
(client/post "http://example.com" {:cookie-policy :standard})
(client/post "http://example.com" {:cookie-policy :standard-strict})

;; See core/get-cookie-policy to extend with your own policy if desired:
(defn my-cookie-policy [cookie origin] (your-validation cookie origin))
(defmethod get-cookie-policy :custom [req] my-cookie-policy)
(client/post "http://example.com" {:cookie-policy :custom})
;; Cookies can be completely configurable with a custom spec by adding a
;; function to return a cookie spec for parsing the cookie. For example, if you
;; wanted to configure a spec provider to have a certain compatibility level:
(client/post "http://example.com"
{:cookie-spec
(fn [http-context]
(println "generating a new cookie spec")
(.create
(org.apache.http.impl.cookie.RFC6265CookieSpecProvider.
org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/IE_MEDIUM_SECURITY
(PublicSuffixMatcherLoader/getDefault))
http-context))})
;; Or a version with relaxed compatibility
(client/post "http://example.com"
{:cookie-spec
(fn [http-context]
(println "generating a new cookie spec")
(.create
(org.apache.http.impl.cookie.RFC6265CookieSpecProvider.
org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/RELAXED
(PublicSuffixMatcherLoader/getDefault))
http-context))})

;; Sometimes you want to do your own validation or something, which you can do
;; by proxying the CookieSpecBase. Note that this doesn't actually return the
;; cookies, because clj-http does its own cookie parsing. If you want to store
;; the cookies from these methods you'll need to use a cookie store or put it in
;; some datastructure yourself.
(client/post "http://example.com"
{:cookie-spec
(fn [http-context]
(proxy [org.apache.http.impl.cookie.CookieSpecBase] []
;; Version and version header
(getVersion [] 0)
(getVersionHeader [] nil)
;; parse headers into cookie objects
(parse [header cookie-origin] (java.util.ArrayList.))
;; Validate a cookie, throwing MalformedCookieException if the
;; cookies isn't valid
(validate [cookie cookie-origin]
(println "validating:" cookie))
;; Determine if a cookie matches the target location
(match [cookie cookie-origin] true)
;; Format a list of cookies into a list of headers
(formatCookies [cookies] (java.util.ArrayList.))))})

;; If you have created your own registry for cookie policies, you can provide
;; :cookie-policy-registry to use it. See
;; clj-http.core/create-custom-cookie-policy-registry for an example of a custom
;; registry
(client/post "http://example.com"
{:cookie-policy-registry my-custom-policy-registry
:cookie-policy "my-policy"})

;; Need to contact a server with an untrusted SSL cert?
(client/get "https://alioth.debian.org" {:insecure? true})
@@ -21,6 +21,11 @@ List of user-visible changes that have gone into each release
be specified (with the same setting) - https://github.com/dakrone/clj-http/issues/441
- Cancelling the =Future= returned from an async http request now also aborts the HttpRequest object
- Async connection managers no longer put the connection manager in an illegal ACTIVE state [[https://github.com/dakrone/clj-http/issues/443][#443]]
- Added the =:cookie-spec= and =:cookie-policy-registry= options for specifying a custom cookie spec
for parsing cookies. Since clj-http doesn't rely on Apache's cookies handling, this is for
advanced users who wish to add their own cookie validation, or use Apache's handling instead of
clj-http's. It also allows a user who wants to registry a custom spec to reuse the spec without
creating it for every request. Semi-related to https://github.com/dakrone/clj-http/issues/444

** 3.8.0
- Reintroduce the =:save-request= and =:debug-body= options
@@ -30,11 +30,14 @@
(org.apache.http.conn.ssl BrowserCompatHostnameVerifier
SSLConnectionSocketFactory SSLContexts)
(org.apache.http.conn.socket PlainConnectionSocketFactory)
(org.apache.http.conn.util PublicSuffixMatcherLoader)
(org.apache.http.cookie CookieSpecProvider)
(org.apache.http.entity ByteArrayEntity StringEntity)
(org.apache.http.impl.client BasicCredentialsProvider
CloseableHttpClient HttpClients
DefaultRedirectStrategy
LaxRedirectStrategy HttpClientBuilder)
(org.apache.http.impl.cookie DefaultCookieSpecProvider)
(org.apache.http.impl.conn SystemDefaultRoutePlanner
DefaultProxyRoutePlanner)
(org.apache.http.impl.nio.client HttpAsyncClientBuilder
@@ -44,6 +47,8 @@
(java.util.concurrent ExecutionException)
(org.apache.http.entity.mime HttpMultipartMode)))

(def CUSTOM_COOKIE_POLICY "_custom")

(defn parse-headers
"Takes a HeaderIterator and returns a map of names to values.
@@ -140,6 +145,18 @@
(handler e cnt context)))))
builder)

(defn create-custom-cookie-policy-registry
"Given a function that will take an HttpContext and return a CookieSpec,
create a new Registry for the cookie policy under the CUSTOM_COOKIE_POLICY
string."
[cookie-spec-fn]
(-> (RegistryBuilder/create)
(.register CUSTOM_COOKIE_POLICY
(proxy [CookieSpecProvider] []
(create [context]
(cookie-spec-fn context))))
(.build)))

(defmulti get-cookie-policy
"Method to retrieve the cookie policy that should be used for the request.
This is a multimethod that may be extended to return your own cookie policy.
@@ -163,7 +180,7 @@
socket-timeout
conn-request-timeout
max-redirects
cookie-policy]
cookie-spec]
:as req}]
(let [config (-> (RequestConfig/custom)
(.setConnectTimeout (or conn-timeout -1))
@@ -175,8 +192,10 @@
(boolean (opt req :allow-circular-redirects)))
(.setRelativeRedirectsAllowed
((complement false?)
(opt req :allow-relative-redirects)))
(.setCookieSpec (get-cookie-policy req)))]
(opt req :allow-relative-redirects))))]
(if cookie-spec
(.setCookieSpec config CUSTOM_COOKIE_POLICY)
(.setCookieSpec config (get-cookie-policy req)))
(when max-redirects (.setMaxRedirects config max-redirects))
(.build config)))

@@ -211,7 +230,8 @@
using proxies."
[{:keys [retry-handler request-interceptor
response-interceptor proxy-host proxy-port
http-builder-fns]
http-builder-fns cookie-spec
cookie-policy-registry]
:as req}
conn-mgr & [http-url proxy-ignore-hosts]]
;; have to let first, otherwise we get a reflection warning on (.build)
@@ -226,6 +246,13 @@
(get-route-planner
proxy-host proxy-port
proxy-ignore-hosts http-url)))]
(when (or cookie-policy-registry cookie-spec)
(if cookie-policy-registry
;; They have a custom registry they'd like to re-use, so use that
(.setDefaultCookieSpecRegistry builder cookie-policy-registry)
;; They have only a one-time function for cookie spec, so use that
(.setDefaultCookieSpecRegistry
builder (create-custom-cookie-policy-registry cookie-spec))))
(when request-interceptor
(.addInterceptorLast
builder (proxy [HttpRequestInterceptor] []
@@ -17,10 +17,14 @@
(org.apache.http.client.protocol HttpClientContext)
(org.apache.http.client.config RequestConfig)
(org.apache.http.client.params CookiePolicy ClientPNames)
(org.apache.http.conn.util PublicSuffixMatcherLoader)
(org.apache.http.cookie CommonCookieAttributeHandler)
(org.apache.http HttpRequest HttpResponse HttpConnection
HttpInetConnection HttpVersion ProtocolException)
(org.apache.http.protocol HttpContext ExecutionContext)
(org.apache.http.impl.client DefaultHttpClient)
(org.apache.http.impl.cookie RFC6265CookieSpec RFC6265CookieSpecProvider
RFC6265CookieSpecProvider$CompatibilityLevel)
(org.apache.http.client.params ClientPNames)
(org.apache.logging.log4j LogManager)
(sun.security.provider.certpath SunCertPathBuilderException)))
@@ -118,7 +122,15 @@
[:propfind "/propfind-with-body"]
{:status 200 :body (:body req)}
[:get "/query-string"]
{:status 200 :body (:query-string req)}))
{:status 200 :body (:query-string req)}
[:get "/cookie"]
{:status 200 :body "yay" :headers {"Set-Cookie" "foo=bar"}}
[:get "/bad-cookie"]
{:status 200 :body "yay"
:headers
{"Set-Cookie"
(str "DD-PSHARD=3; expires=\"Thu, 12-Apr-2018 06:40:25 GMT\"; "
"Max-Age=604800; Path=/; secure; HttpOnly")}}))

(defn add-headers-if-requested [client]
(fn [req]
@@ -771,6 +783,7 @@
(catch TimeoutException te)))

(deftest ^:integration test-reusable-http-client
(run-server)
(let [cm (conn/make-reuseable-async-conn-manager {})
hc (core/build-async-http-client {} cm)]
(client/get (localhost "/json")
@@ -791,3 +804,33 @@
:as :json})]
(is (= 200 (:status resp)))
(is (= {:foo "bar"} (:body resp)))))

(deftest ^:integration t-cookies-spec
(run-server)
(try
(client/get (localhost "/bad-cookie"))
(is false "should have failed")
(catch org.apache.http.cookie.MalformedCookieException e))
(client/get (localhost "/bad-cookie") {:decode-cookies false})
(let [validated (atom false)
spec-provider (RFC6265CookieSpecProvider.)
resp (client/get (localhost "/cookie")
{:cookie-spec
(fn [http-context]
(proxy [org.apache.http.impl.cookie.CookieSpecBase] []
;; Version and version header
(getVersion [] 0)
(getVersionHeader [] nil)
;; parse headers into cookie objects
(parse [header cookie-origin]
(.parse (.create spec-provider http-context)
header cookie-origin))
;; Validate a cookie, throwing MalformedCookieException if the
;; cookies isn't valid
(validate [cookie cookie-origin]
(reset! validated true))
;; Determine if a cookie matches the target location
(match [cookie cookie-origin] true)
;; Format a list of cookies into a list of headers
(formatCookies [cookies] (java.util.ArrayList.))))})]
(is (= @validated true))))

0 comments on commit 2608c30

Please sign in to comment.
You can’t perform that action at this time.