Permalink
1 comment
on commit
sign in to comment.
Showing
with
174 additions
and 1 deletion.
- +115 −0 src/ring/middleware/cookies.clj
- +57 −0 test/ring/middleware/cookies_test.clj
- +2 −1 test/run.clj
115
src/ring/middleware/cookies.clj
| @@ -0,0 +1,115 @@ | ||
| +(ns ring.middleware.cookies | ||
| + (:use clojure.contrib.def) | ||
| + (:use clojure.contrib.java-utils)) | ||
| + | ||
| +(defvar- re-token #"[!#$%&'*\-+.0-9A-Z\^_`a-z\|~]+" | ||
| + "HTTP token: 1*<any CHAR except CTLs or tspecials>. See RFC2068") | ||
| + | ||
| +(defvar- re-quoted #"\"(\\\"|[^\"])*\"" | ||
| + "HTTP quoted-string: <\"> *<any TEXT except \"> <\">. See RFC2068.") | ||
| + | ||
| +(defvar- re-value (str re-token "|" re-quoted) | ||
| + "HTTP value: token | quoted-string. See RFC2109") | ||
| + | ||
| +(defvar- re-cookie | ||
| + (re-pattern (str "\\s*(" re-token ")=(" re-value ")\\s*[;,]?")) | ||
| + "HTTP cookie-value: NAME \"=\" VALUE") | ||
| + | ||
| +(defvar- cookie-attrs | ||
| + {"$Path" :path, "$Domain" :domain, "$Port" :port} | ||
| + "Special attributes defined by RFC2109 and RFC2965 that apply to the | ||
| + Cookie header.") | ||
| + | ||
| +(defvar- set-cookie-attrs | ||
| + {:comment "Comment", :comment-url "CommentURL", :discard "Discard", | ||
| + :domain "Domain", :max-age "Max-Age", :path "Path", :port "Port", | ||
| + :secure "Secure", :version "Version"} | ||
| + "Attributes defined by RFC2109 and RFC2965 that apply to the Set-Cookie | ||
| + header.") | ||
| + | ||
| +(defn- parse-cookie-header | ||
| + "Turn a HTTP Cookie header into a list of name/value pairs." | ||
| + [header] | ||
| + (for [[_ name value] (re-seq re-cookie header)] | ||
| + [name value])) | ||
| + | ||
| +(defn- normalize-quoted-strs | ||
| + "Turn quoted strings into normal Clojure strings using read-string." | ||
| + [cookies] | ||
| + (for [[name value] cookies] | ||
| + (if (.startsWith #^String value "\"") | ||
| + [name (read-string value)] | ||
| + [name value]))) | ||
| + | ||
| +(defn- get-cookie | ||
| + "Get a single cookie from a sequence of cookie-values" | ||
| + [[[name value] & cookie-values]] | ||
| + {name (reduce | ||
| + (fn [m [k v]] (assoc m (cookie-attrs k) v)) | ||
| + {:value value} | ||
| + (take-while (comp cookie-attrs first) cookie-values))}) | ||
| + | ||
| +(defn- to-cookie-map | ||
| + "Turn a sequence of cookie-values into a cookie map." | ||
| + [values] | ||
| + (loop [values values, cookie-map {}] | ||
| + (if (seq values) | ||
| + (let [cookie (get-cookie values)] | ||
| + (recur | ||
| + (drop (-> cookie first val count) values) | ||
| + (merge cookie-map cookie))) | ||
| + cookie-map))) | ||
| + | ||
| +(defn- parse-cookies | ||
| + "Parse the cookies from a request map." | ||
| + [request] | ||
| + (if-let [cookie (get-in request [:headers "cookie"])] | ||
| + (-> cookie | ||
| + parse-cookie-header | ||
| + normalize-quoted-strs | ||
| + to-cookie-map | ||
| + (dissoc "$Version")))) | ||
| + | ||
| +(defn- write-attr | ||
| + "Turn a name-value pair into a cookie attr string." | ||
| + [name value] | ||
| + (str (as-str name) "=" (pr-str value))) | ||
| + | ||
| +(defn- write-attr-map | ||
| + "Write a map of cookie attributes to a string." | ||
| + [attrs] | ||
| + (for [[key value] attrs] | ||
| + (let [name (set-cookie-attrs key)] | ||
| + (cond | ||
| + (true? value) (str ";" name) | ||
| + (false? value) "" | ||
| + :else (str ";" (write-attr name value)))))) | ||
| + | ||
| +(defn- write-cookies | ||
| + "Turn a map of cookies into a seq of strings for a Set-Cookie header." | ||
| + [cookies] | ||
| + (for [[name value] cookies] | ||
| + (if (map? value) | ||
| + (apply str (write-attr name (:value value)) | ||
| + (write-attr-map (dissoc value :value))) | ||
| + (write-attr name value)))) | ||
| + | ||
| +(defn- set-cookies | ||
| + "Add a Set-Cookie header to a response if there is a :cookies key." | ||
| + [response] | ||
| + (if-let [cookies (:cookies response)] | ||
| + (assoc-in response | ||
| + [:headers "Set-Cookie"] | ||
| + (write-cookies cookies)) | ||
| + response)) | ||
| + | ||
| +(defn wrap-cookies | ||
| + "Parses the cookies in the request map, then assocs the resulting map | ||
| + to the :cookies key on the request." | ||
| + [handler] | ||
| + (fn [request] | ||
| + (let [request (assoc request :cookies (parse-cookies request)) | ||
| + response (handler request)] | ||
| + (-> response | ||
| + set-cookies | ||
| + (dissoc :cookies))))) |
57
test/ring/middleware/cookies_test.clj
| @@ -0,0 +1,57 @@ | ||
| +(ns ring.middleware.cookies-test | ||
| + (:use clj-unit.core | ||
| + ring.middleware.cookies)) | ||
| + | ||
| +(deftest "wrap-cookies: basic cookie" | ||
| + (let [req {:headers {"cookie" "a=b"}} | ||
| + resp ((wrap-cookies :cookies) req)] | ||
| + (assert= {"a" {:value "b"}} resp))) | ||
| + | ||
| +(deftest "wrap-cookies: multiple cookies" | ||
| + (let [req {:headers {"cookie" "a=b; c=d,e=f"}} | ||
| + resp ((wrap-cookies :cookies) req)] | ||
| + (assert= {"a" {:value "b"}, "c" {:value "d"}, "e" {:value "f"}} | ||
| + resp))) | ||
| + | ||
| +(deftest "wrap-cookies: quoted cookies" | ||
| + (let [req {:headers {"cookie" "a=\"b=c;e=f\""}} | ||
| + resp ((wrap-cookies :cookies) req)] | ||
| + (assert= {"a" {:value "b=c;e=f"}} | ||
| + resp))) | ||
| + | ||
| +(deftest "wrap-cookies: escaped quotes" | ||
| + (let [req {:headers {"cookie" "a=\"\\\"b\\\"\""}} | ||
| + resp ((wrap-cookies :cookies) req)] | ||
| + (assert= {"a" {:value "\"b\""}} | ||
| + resp))) | ||
| + | ||
| +(deftest "wrap-cookies: extra attrs" | ||
| + (let [req {:headers {"cookie" "a=b;$Path=\"/\";$Domain=localhost"}} | ||
| + resp ((wrap-cookies :cookies) req)] | ||
| + (assert= {"a" {:value "b", :path "/", :domain "localhost"}} | ||
| + resp))) | ||
| + | ||
| +(deftest "wrap-cookies: set basic cookie" | ||
| + (let [handler (constantly {:cookies {"a" "b"}}) | ||
| + resp ((wrap-cookies handler) {})] | ||
| + (assert= {"Set-Cookie" (list "a=\"b\"")} | ||
| + (:headers resp)))) | ||
| + | ||
| +(deftest "wrap-cookies: set multiple cookies" | ||
| + (let [handler (constantly {:cookies {"a" "b", "c" "d"}}) | ||
| + resp ((wrap-cookies handler) {})] | ||
| + (assert= {"Set-Cookie" (list "a=\"b\"" "c=\"d\"")} | ||
| + (:headers resp)))) | ||
| + | ||
| +(deftest "wrap-cookies: set keyword cookie" | ||
| + (let [handler (constantly {:cookies {:a "b"}}) | ||
| + resp ((wrap-cookies handler) {})] | ||
| + (assert= {"Set-Cookie" (list "a=\"b\"")} | ||
| + (:headers resp)))) | ||
| + | ||
| +(deftest "wrap-cookies: set extra attrs" | ||
| + (let [cookies {"a" {:value "b", :path "/", :secure true}} | ||
| + handler (constantly {:cookies cookies}) | ||
| + resp ((wrap-cookies handler) {})] | ||
| + (assert= {"Set-Cookie" (list "a=\"b\";Path=\"/\";Secure")} | ||
| + (:headers resp)))) |
3
test/run.clj
f365d5cI am so glad this is happening — I really wanted to have cookies and sessions in ring.