Permalink
Please sign in to comment.
Showing
with
283 additions
and 4 deletions.
- +1 −0 project.clj
- +5 −4 src/ring/middleware/cookies.clj
- +38 −0 src/ring/middleware/session.clj
- +104 −0 src/ring/middleware/session/cookie.clj
- +17 −0 src/ring/middleware/session/memory.clj
- +33 −0 test/ring/middleware/session/cookie_test.clj
- +30 −0 test/ring/middleware/session/memory_test.clj
- +55 −0 test/ring/middleware/session_test.clj
1
project.clj
9
src/ring/middleware/cookies.clj
38
src/ring/middleware/session.clj
| @@ -0,0 +1,38 @@ | ||
| +(ns ring.middleware.session | ||
| + (:use ring.middleware.cookies | ||
| + ring.middleware.session.memory)) | ||
| + | ||
| +(defn wrap-session | ||
| + "Reads in the current HTTP session map, and adds it to the :session key on | ||
| + the request. If a :session key is added to the response by the handler, the | ||
| + session is updated with the new value. If the value is nil, the session is | ||
| + deleted. | ||
| + | ||
| + The following options are available: | ||
| + :store | ||
| + An implementation map containing :read, :write, and :delete | ||
| + keys. This determines how the session is stored. Defaults to | ||
| + in-memory storage. | ||
| + :cookie-name | ||
| + The name of the cookie that holds the session key. Defaults to | ||
| + \"ring-session\"" | ||
| + ([handler] | ||
| + (wrap-session handler {})) | ||
| + ([handler options] | ||
| + (let [store (options :store (memory-store)) | ||
| + cookie (options :cookie-name "ring-session")] | ||
| + (wrap-cookies | ||
| + (fn [request] | ||
| + (let [sess-key (get-in request [:cookies cookie :value]) | ||
| + session ((store :read) sess-key) | ||
| + request (assoc request :session session) | ||
| + response (handler request) | ||
| + sess-key* (if (contains? response :session) | ||
| + (if (response :session) | ||
| + ((store :write) sess-key (response :session)) | ||
| + (if sess-key | ||
| + ((store :delete) sess-key)))) | ||
| + response (dissoc response :session)] | ||
| + (if (and sess-key* (not= sess-key sess-key*)) | ||
| + (assoc response :cookies {cookie sess-key*}) | ||
| + response))))))) |
104
src/ring/middleware/session/cookie.clj
| @@ -0,0 +1,104 @@ | ||
| +(ns ring.middleware.session.cookie | ||
| + "Encrypted cookie session storage." | ||
| + (:use clojure.contrib.def) | ||
| + (:use clojure.contrib.base64) | ||
| + (:import java.security.SecureRandom | ||
| + [javax.crypto Cipher Mac] | ||
| + [javax.crypto.spec SecretKeySpec IvParameterSpec] | ||
| + org.apache.commons.codec.binary.Base64)) | ||
| + | ||
| +(defvar- seed-algorithm "SHA1PRNG" | ||
| + "Algorithm to seed random numbers.") | ||
| + | ||
| +(defvar- hmac-algorithm "HmacSHA256" | ||
| + "Algorithm to generate a HMAC.") | ||
| + | ||
| +(defvar- crypt-type "AES" | ||
| + "Type of encryption to use.") | ||
| + | ||
| +(defvar- crypt-algorithm "AES/CBC/PKCS5Padding" | ||
| + "Full algorithm to encrypt data with.") | ||
| + | ||
| +(defn- secure-random-bytes | ||
| + "Returns a random byte array of the specified size." | ||
| + [size] | ||
| + (let [seed (byte-array size)] | ||
| + (.nextBytes (SecureRandom/getInstance seed-algorithm) seed) | ||
| + seed)) | ||
| + | ||
| +(defn- base64-encode | ||
| + "Encode an array of bytes into a base64 encoded string." | ||
| + [unencoded] | ||
| + (String. (Base64/encodeBase64 unencoded))) | ||
| + | ||
| +(defn- base64-decode | ||
| + "Decode a base64 encoded string into an array of bytes." | ||
| + [encoded] | ||
| + (Base64/decodeBase64 (.getBytes encoded))) | ||
| + | ||
| +(defn- hmac | ||
| + "Generates a Base64 HMAC with the supplied key on a string of data." | ||
| + [key data] | ||
| + (let [mac (Mac/getInstance hmac-algorithm)] | ||
| + (.init mac (SecretKeySpec. key hmac-algorithm)) | ||
| + (base64-encode (.doFinal mac data)))) | ||
| + | ||
| +(defn- encrypt | ||
| + "Encrypt a string with a key." | ||
| + [key data] | ||
| + (let [cipher (Cipher/getInstance crypt-algorithm) | ||
| + secret-key (SecretKeySpec. key crypt-type) | ||
| + iv (secure-random-bytes (.getBlockSize cipher))] | ||
| + (.init cipher Cipher/ENCRYPT_MODE secret-key (IvParameterSpec. iv)) | ||
| + (->> (.doFinal cipher data) | ||
| + (concat iv) | ||
| + (byte-array)))) | ||
| + | ||
| +(defn- decrypt | ||
| + "Decrypt an array of bytes with a key." | ||
| + [key data] | ||
| + (let [cipher (Cipher/getInstance crypt-algorithm) | ||
| + secret-key (SecretKeySpec. key crypt-type) | ||
| + [iv data] (split-at (.getBlockSize cipher) data) | ||
| + iv-spec (IvParameterSpec. (byte-array iv))] | ||
| + (.init cipher Cipher/DECRYPT_MODE secret-key iv-spec) | ||
| + (String. (.doFinal cipher (byte-array data))))) | ||
| + | ||
| +(defn- get-secret-key | ||
| + "Get a valid secret key from a map of options, or create a random one from | ||
| + scratch." | ||
| + [options] | ||
| + (if-let [secret-key (:key options)] | ||
| + (if (string? secret-key) | ||
| + (.getBytes secret-key) | ||
| + secret-key) | ||
| + (secure-random-bytes 16))) | ||
| + | ||
| +(defn- seal | ||
| + "Seal a Clojure data structure into an encrypted and HMACed string." | ||
| + [key data] | ||
| + (let [data (encrypt key (.getBytes (pr-str data)))] | ||
| + (str (base64-encode data) "--" (hmac key data)))) | ||
| + | ||
| +(defn- unseal | ||
| + "Retrieve a sealed Clojure data structure from a string" | ||
| + [key string] | ||
| + (let [[data mac] (.split string "--") | ||
| + data (base64-decode data)] | ||
| + (if (= mac (hmac key data)) | ||
| + (read-string (decrypt key data))))) | ||
| + | ||
| +(defn cookie-store | ||
| + "Creates an encrypted cookie storage engine." | ||
| + ([] | ||
| + (cookie-store {})) | ||
| + ([options] | ||
| + (let [secret-key (get-secret-key options)] | ||
| + {:read (fn [session-data] | ||
| + (if session-data | ||
| + (or (unseal secret-key session-data) {}) | ||
| + {})) | ||
| + :write (fn [_ session] | ||
| + (seal secret-key session)) | ||
| + :delete (fn [_] | ||
| + (seal secret-key {}))}))) |
17
src/ring/middleware/session/memory.clj
| @@ -0,0 +1,17 @@ | ||
| +(ns ring.middleware.session.memory | ||
| + "In-memory session storage." | ||
| + (:import java.util.UUID)) | ||
| + | ||
| +(defn memory-store | ||
| + "Creates an in-memory session storage engine." | ||
| + [] | ||
| + (let [session-map (atom {})] | ||
| + {:read (fn [session-key] | ||
| + (@session-map session-key {})) | ||
| + :write (fn [session-key session] | ||
| + (let [session-key (or session-key (str (UUID/randomUUID)))] | ||
| + (swap! session-map assoc session-key session) | ||
| + session-key)) | ||
| + :delete (fn [session-key] | ||
| + (swap! session-map dissoc session-key) | ||
| + nil)})) |
33
test/ring/middleware/session/cookie_test.clj
| @@ -0,0 +1,33 @@ | ||
| +(ns ring.middleware.session.cookie-test | ||
| + (:use clojure.test | ||
| + ring.middleware.session.cookie)) | ||
| + | ||
| +(deftest cookie-session-read-not-exist | ||
| + (let [store (cookie-store)] | ||
| + (is ((:read store) "non-existent") | ||
| + {}))) | ||
| + | ||
| +(deftest cookie-session-create | ||
| + (let [store (cookie-store) | ||
| + sess-key ((:write store) nil {:foo "bar"})] | ||
| + (is (not (nil? sess-key))) | ||
| + (is (= ((:read store) sess-key) | ||
| + {:foo "bar"})))) | ||
| + | ||
| +(deftest cookie-session-update | ||
| + (let [store (cookie-store) | ||
| + sess-key ((:write store) nil {:foo "bar"}) | ||
| + sess-key* ((:write store) sess-key {:bar "baz"})] | ||
| + (is (not (nil? sess-key*))) | ||
| + (is (not= sess-key sess-key*)) | ||
| + (is (= ((:read store) sess-key*) | ||
| + {:bar "baz"})))) | ||
| + | ||
| +(deftest cookie-session-delete | ||
| + (let [store (cookie-store) | ||
| + sess-key ((:write store) nil {:foo "bar"}) | ||
| + sess-key* ((:delete store) sess-key)] | ||
| + (is (not (nil? sess-key*))) | ||
| + (is (not= sess-key sess-key*)) | ||
| + (is (= ((:read store) sess-key*) | ||
| + {})))) |
30
test/ring/middleware/session/memory_test.clj
| @@ -0,0 +1,30 @@ | ||
| +(ns ring.middleware.session.memory-test | ||
| + (:use clojure.test | ||
| + ring.middleware.session.memory)) | ||
| + | ||
| +(deftest memory-session-read-not-exist | ||
| + (let [store (memory-store)] | ||
| + (is ((:read store) "non-existent") | ||
| + {}))) | ||
| + | ||
| +(deftest memory-session-create | ||
| + (let [store (memory-store) | ||
| + sess-key ((:write store) nil {:foo "bar"})] | ||
| + (is (not (nil? sess-key))) | ||
| + (is (= ((:read store) sess-key) | ||
| + {:foo "bar"})))) | ||
| + | ||
| +(deftest memory-session-update | ||
| + (let [store (memory-store) | ||
| + sess-key ((:write store) nil {:foo "bar"}) | ||
| + sess-key* ((:write store) sess-key {:bar "baz"})] | ||
| + (is (= sess-key sess-key*)) | ||
| + (is (= ((:read store) sess-key) | ||
| + {:bar "baz"})))) | ||
| + | ||
| +(deftest memory-session-delete | ||
| + (let [store (memory-store) | ||
| + sess-key ((:write store) nil {:foo "bar"})] | ||
| + (is (nil? ((:delete store) sess-key))) | ||
| + (is (= ((:read store) sess-key) | ||
| + {})))) |
55
test/ring/middleware/session_test.clj
| @@ -0,0 +1,55 @@ | ||
| +(ns ring.middleware.session-test | ||
| + (:use clojure.test | ||
| + clojure.contrib.mock | ||
| + ring.middleware.session)) | ||
| + | ||
| +(declare reader writer deleter) | ||
| + | ||
| +(deftest session-is-read | ||
| + (expect [reader (times 1 | ||
| + (has-args [(partial = "test")] | ||
| + (returns {:bar "foo"}))) | ||
| + writer (times never) | ||
| + deleter (times never)] | ||
| + (let [store {:read reader, :write writer, :delete deleter} | ||
| + handler (fn [req] | ||
| + (is (= (req :session) {:bar "foo"})) | ||
| + {}) | ||
| + handler (wrap-session handler {:store store})] | ||
| + (handler {:cookies {"ring-session" {:value "test"}}})))) | ||
| + | ||
| +(deftest session-is-written | ||
| + (expect [reader (times 1 (returns {})) | ||
| + writer (times 1 (has-args [nil? (partial = {:foo "bar"})])) | ||
| + deleter (times never)] | ||
| + (let [store {:read reader, :write writer, :delete deleter} | ||
| + handler (constantly {:session {:foo "bar"}}) | ||
| + handler (wrap-session handler {:store store})] | ||
| + (handler {:cookies {}})))) | ||
| + | ||
| +(deftest session-is-deleted | ||
| + (expect [reader (times 1 (returns {})) | ||
| + writer (times never) | ||
| + deleter (times 1 (has-args [(partial = "test")]))] | ||
| + (let [store {:read reader, :write writer, :delete deleter} | ||
| + handler (constantly {:session nil}) | ||
| + handler (wrap-session handler {:store store})] | ||
| + (handler {:cookies {"ring-session" {:value "test"}}})))) | ||
| + | ||
| +(deftest session-write-outputs-cookie | ||
| + (let [store {:read (constantly {}) | ||
| + :write (constantly "foo:bar")} | ||
| + handler (constantly {:session {:foo "bar"}}) | ||
| + handler (wrap-session handler {:store store}) | ||
| + response (handler {:cookies {}})] | ||
| + (is (= (get-in response [:headers "Set-Cookie"]) | ||
| + ["ring-session=\"foo:bar\""])))) | ||
| + | ||
| +(deftest session-delete-outputs-cookie | ||
| + (let [store {:read (constantly {:foo "bar"}) | ||
| + :delete (constantly "deleted")} | ||
| + handler (constantly {:session nil}) | ||
| + handler (wrap-session handler {:store store}) | ||
| + response (handler {:cookies {"ring-session" {:value "foo:bar"}}})] | ||
| + (is (= (get-in response [:headers "Set-Cookie"]) | ||
| + ["ring-session=\"deleted\""])))) |
0 comments on commit
d685b93