Skip to content

Commit

Permalink
Added the wrap-cookies middleware.
Browse files Browse the repository at this point in the history
  • Loading branch information
Roman Scherer committed Sep 8, 2011
1 parent 65eae47 commit b693ae3
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/clj_http/client.clj
Expand Up @@ -3,6 +3,7 @@
(:require [clojure.string :as str]
[clj-http.core :as core]
[clj-http.util :as util])
(:use [clj-http.cookies :only (wrap-cookies)])
(:import (java.net URL))
(:refer-clojure :exclude (get)))

Expand Down Expand Up @@ -184,7 +185,8 @@
wrap-accept-encoding
wrap-content-type
wrap-method
wrap-url))
wrap-url
wrap-cookies))

(def #^{:doc
"Executes the HTTP request corresponding to the given map and returns
Expand Down
102 changes: 102 additions & 0 deletions src/clj_http/cookies.clj
@@ -0,0 +1,102 @@
(ns clj-http.cookies
(:import (org.apache.http.client.params ClientPNames CookiePolicy)
(org.apache.http.cookie CookieOrigin)
(org.apache.http.params BasicHttpParams)
(org.apache.http.impl.cookie BasicClientCookie2)
(org.apache.http.impl.cookie BrowserCompatSpecFactory)
(org.apache.http.message BasicHeader))
(:use [clojure.contrib.string :only (as-str blank? join lower-case)]
clj-http.util))

(defn- cookie-spec []
(.newInstance
(BrowserCompatSpecFactory.)
(doto (BasicHttpParams.)
(.setParameter ClientPNames/COOKIE_POLICY CookiePolicy/BROWSER_COMPATIBILITY))))

(defn- compact-map
"Removes all map entries where value is nil."
[m] (reduce #(if (get m %2) (assoc %1 %2 (get m %2)) %1) (sorted-map) (sort (keys m))))

(defn- to-cookie
"Converts a ClientCookie object into a tuple where the first item is
the name of the cookie and the second item the content of the
cookie."
[cookie]
[(.getName cookie)
(compact-map
{:comment (.getComment cookie)
:comment-url (.getCommentURL cookie)
:discard (not (.isPersistent cookie))
:domain (.getDomain cookie)
:expires (if (.getExpiryDate cookie) (.getExpiryDate cookie))
:path (.getPath cookie)
:ports (if (.getPorts cookie) (seq (.getPorts cookie)))
:secure (.isSecure cookie)
:value (url-decode (.getValue cookie))
:version (.getVersion cookie)})])

(defn- to-basic-client-cookie
"Converts a cookie seq into a BasicClientCookie2."
[[cookie-name cookie-content]]
(doto (BasicClientCookie2. (as-str cookie-name) (url-encode (as-str (:value cookie-content))))
(.setComment (:comment cookie-content))
(.setCommentURL (:comment-url cookie-content))
(.setDiscard (or (:discard cookie-content) true))
(.setDomain (:domain cookie-content))
(.setExpiryDate (:expires cookie-content))
(.setPath (:path cookie-content))
(.setPorts (int-array (:ports cookie-content)))
(.setSecure (or (:secure cookie-content) false))
(.setVersion (or (:version cookie-content) 0))))

(defn decode-cookie
"Decode the Set-Cookie string into a cookie seq."
[set-cookie-str]
(if-not (blank? set-cookie-str)
(let [domain (lower-case (str (gensym))) ; I just want to parse a cookie without providing origin. How?
origin (CookieOrigin. domain 80 "/" false)
[cookie-name cookie-content] (to-cookie (first (.parse (cookie-spec) (BasicHeader. "set-cookie" set-cookie-str) origin)))]
[cookie-name
(if (= domain (:domain cookie-content))
(dissoc cookie-content :domain) cookie-content)])))

(defn decode-cookies
"Converts a cookie string or seq of strings into a cookie map."
[cookies]
(reduce #(assoc %1 (first %2) (second %2)) {}
(map decode-cookie (if (sequential? cookies) cookies [cookies]))))

(defn decode-cookie-header
"Decode the Set-Cookie header into the cookies key."
[response]
(if-let [cookies (get (:headers response) "set-cookie")]
(assoc response
:cookies (decode-cookies cookies)
:headers (dissoc (:headers response) "set-cookie"))
response))

(defn encode-cookie
"Encode the cookie into a string used by the Cookie header."
[cookie]
(if-let [header (first (.formatCookies (cookie-spec) [(to-basic-client-cookie cookie)]))]
(.getValue header)))

(defn encode-cookies
"Encode the cookie map into a string."
[cookie-map] (join ";" (map encode-cookie (seq cookie-map))))

(defn encode-cookie-header
"Encode the :cookies key of the request into a Cookie header."
[request]
(if (:cookies request)
(-> request
(assoc-in [:headers "Cookie"] (encode-cookies (:cookies request)))
(dissoc :cookies))
request))

(defn wrap-cookies
[client]
(fn [request]
(let [response (client (encode-cookie-header request))]
(decode-cookie-header response))))
8 changes: 7 additions & 1 deletion src/clj_http/util.clj
Expand Up @@ -3,7 +3,7 @@
(:import (org.apache.commons.codec.binary Base64)
(org.apache.commons.io IOUtils)
(java.io ByteArrayInputStream ByteArrayOutputStream)
(java.net URLEncoder)
(java.net URLEncoder URLDecoder)
(java.util.zip InflaterInputStream DeflaterInputStream
GZIPInputStream GZIPOutputStream)))

Expand All @@ -17,6 +17,12 @@
[#^"[B" b]
(String. b "UTF-8"))

(defn url-decode
"Returns the form-url-decoded version of the given string, using either a
specified encoding or UTF-8 by default."
[encoded & [encoding]]
(URLDecoder/decode encoded (or encoding "UTF-8")))

(defn url-encode
"Returns an UTF-8 URL encoded version of the given string."
[unencoded]
Expand Down
189 changes: 189 additions & 0 deletions test/clj_http/cookies_test.clj
@@ -0,0 +1,189 @@
(ns clj-http.cookies-test
(:import (org.apache.http.impl.cookie BasicClientCookie BasicClientCookie2))
(:use clj-http.cookies
clj-http.util
clojure.test))

(defn refer-private [ns]
(doseq [[symbol var] (ns-interns ns)]
(when (:private (meta var))
(intern *ns* symbol var))))

(refer-private 'clj-http.cookies)

(def session "ltQGXSNp7cgNeFG6rPE06qzriaI+R8W7zJKFu4UOlX4=--lWgojFmZlDqSBnYJlUmwhqXL4OgBTkra5WXzi74v+nE=")

(deftest test-compact-map
(are [map expected]
(is (= expected (compact-map map)))
{:a nil :b 2 :c 3 :d nil}
{:b 2 :c 3}
{:comment nil :domain "example.com" :path "/" :ports [80 8080] :value 1}
{:domain "example.com" :path "/" :ports [80 8080] :value 1}))

(deftest test-decode-cookie
(are [set-cookie-str expected]
(is (= expected (decode-cookie set-cookie-str)))
nil nil
"" nil
"example-cookie=example-value;Path=/"
["example-cookie" {:discard true :path "/" :value "example-value" :version 0}]
"example-cookie=example-value;Domain=.example.com;Path=/"
["example-cookie" {:discard true :domain ".example.com" :path "/" :value "example-value" :version 0}]))

(deftest test-decode-cookies-with-seq
(let [cookies (decode-cookies [(str "ring-session=" (url-encode session))])]
(is (map? cookies))
(is (= 1 (count cookies)))
(let [cookie (get cookies "ring-session")]
(is (= true (:discard cookie)))
(is (nil? (:domain cookie)))
(is (= "/" (:path cookie)))
(is (= session (:value cookie)))
(is (= 0 (:version cookie))))))

(deftest test-decode-cookies-with-string
(let [cookies (decode-cookies (str "ring-session=" (url-encode session) ";Path=/"))]
(is (map? cookies))
(is (= 1 (count cookies)))
(let [cookie (get cookies "ring-session")]
(is (= true (:discard cookie)))
(is (nil? (:domain cookie)))
(is (= "/" (:path cookie)))
(is (= session (:value cookie)))
(is (= 0 (:version cookie))))))

(deftest test-decode-cookie-header
(are [response expected]
(is (= expected (decode-cookie-header response)))
{:headers {"set-cookie" "a=1"}}
{:cookies {"a" {:discard true, :path "/", :value "1", :version 0}}, :headers {}}
{:headers {"set-cookie" (str "ring-session=" (url-encode session) ";Path=/")}}
{:cookies {"ring-session" {:discard true, :path "/", :value session, :version 0}}, :headers {}}))

(deftest test-encode-cookie
(are [cookie expected]
(is (= expected (encode-cookie cookie)))
[:a {:value "b"}] "a=b"
["a" {:value "b"}] "a=b"
["example-cookie" {:domain ".example.com" :path "/" :value "example-value"}] "example-cookie=example-value"
["ring-session" {:value session}] (str "ring-session=" (url-encode session))))

(deftest test-encode-cookies
(are [cookie expected]
(is (= expected (encode-cookies cookie)))
{:a {:value "b"} :c {:value "d"} :e {:value "f"}}
"a=b;c=d;e=f"
{"a" {:value "b"} "c" {:value "d"} "e" {:value "f"}}
"a=b;c=d;e=f"
{"example-cookie" {:domain ".example.com" :path "/" :value "example-value"}}
"example-cookie=example-value"
{"example-cookie" {:domain ".example.com" :path "/" :value "example-value" :discard true :version 0}}
"example-cookie=example-value"
{"ring-session" {:value session}}
(str "ring-session=" (url-encode session))))

(deftest test-encode-cookie-header
(are [request expected]
(is (= expected (encode-cookie-header request)))
{:cookies {"a" {:value "1"}}}
{:headers {"Cookie" "a=1"}}
{:cookies {"example-cookie" {:domain ".example.com" :path "/" :value "example-value"}}}
{:headers {"Cookie" "example-cookie=example-value"}}))

(deftest test-to-basic-client-cookie-with-simple-cookie
(let [cookie (to-basic-client-cookie
["ring-session"
{:value session
:path "/"
:domain "example.com"}])]
(is (= "ring-session" (.getName cookie)))
(is (= (url-encode session) (.getValue cookie)))
(is (= "/" (.getPath cookie)))
(is (= "example.com" (.getDomain cookie)))
(is (nil? (.getComment cookie)))
(is (nil? (.getCommentURL cookie)))
(is (not (.isPersistent cookie)))
(is (nil? (.getExpiryDate cookie)))
(is (nil? (seq (.getPorts cookie))))
(is (not (.isSecure cookie)))
(is (= 0 (.getVersion cookie)))))

(deftest test-to-basic-client-cookie-with-full-cookie
(let [cookie (to-basic-client-cookie
["ring-session"
{:value session
:path "/"
:domain "example.com"
:comment "Example Comment"
:comment-url "http://example.com/cookies"
:discard true
:expires (java.util.Date. (long 0))
:ports [80 8080]
:secure true
:version 0}])]
(is (= "ring-session" (.getName cookie)))
(is (= (url-encode session) (.getValue cookie)))
(is (= "/" (.getPath cookie)))
(is (= "example.com" (.getDomain cookie)))
(is (= "Example Comment" (.getComment cookie)))
(is (= "http://example.com/cookies" (.getCommentURL cookie)))
(is (not (.isPersistent cookie)))
(is (= (java.util.Date. (long 0)) (.getExpiryDate cookie)))
(is (= [80 8080] (seq (.getPorts cookie))))
(is (.isSecure cookie))
(is (= 0 (.getVersion cookie)))))

(deftest test-to-basic-client-cookie-with-symbol-as-name
(let [cookie (to-basic-client-cookie [:ring-session {:value session :path "/" :domain "example.com"}])]
(is (= "ring-session" (.getName cookie)))))

(deftest test-to-cookie-with-simple-cookie
(let [[name content]
(to-cookie
(doto (BasicClientCookie. "example-cookie" "example-value")
(.setDomain "example.com")
(.setPath "/")))]
(is (= "example-cookie" name))
(is (nil? (:comment content)))
(is (nil? (:comment-url content)))
(is (:discard content))
(is (= "example.com" (:domain content)))
(is (nil? (:expires content)))
(is (nil? (:ports content)))
(is (not (:secure content)))
(is (= 0 (:version content)))
(is (= "example-value" (:value content)))))

(deftest test-to-cookie-with-full-cookie
(let [[name content]
(to-cookie
(doto (BasicClientCookie2. "example-cookie" "example-value")
(.setComment "Example Comment")
(.setCommentURL "http://example.com/cookies")
(.setDiscard true)
(.setDomain "example.com")
(.setExpiryDate (java.util.Date. (long 0)))
(.setPath "/")
(.setPorts (int-array [80 8080]))
(.setSecure true)
(.setVersion 1)))]
(is (= "example-cookie" name))
(is (= "Example Comment" (:comment content)))
(is (= "http://example.com/cookies" (:comment-url content)))
(is (= true (:discard content)))
(is (= "example.com" (:domain content)))
(is (= (java.util.Date. (long 0)) (:expires content)))
(is (= [80 8080] (:ports content)))
(is (= true (:secure content)))
(is (= 1 (:version content)))
(is (= "example-value" (:value content)))))

(deftest test-wrap-cookies
(is (= {:cookies {"example-cookie" {:discard true :domain ".example.com" :path "/" :value "example-value" :version 0}} :headers {}}
((wrap-cookies
(fn [request]
(is (= (get (:headers request) "Cookie") "a=1;b=2"))
{:headers {"set-cookie" "example-cookie=example-value;Domain=.example.com;Path=/"}}))
{:cookies {:a {:value "1"} :b {:value "2"}}}))))

0 comments on commit b693ae3

Please sign in to comment.