Permalink
Browse files

Added functional session middleware and unit tests

  • Loading branch information...
1 parent 3e71902 commit d685b936fcf04f7575fcae8329885d1354693948 @weavejester weavejester committed Feb 20, 2010
View
1 project.clj
@@ -8,6 +8,7 @@
[org.mortbay.jetty/servlet-api-2.5 "6.1.14"]
[org.apache.httpcomponents/httpcore "4.0.1"]
[org.apache.httpcomponents/httpcore-nio "4.0.1"]
+ [commons-codec "1.4"]
[clj-html "0.1.0-SNAPSHOT"]
[clj-stacktrace "0.1.0-SNAPSHOT"]]
:repositories [["mvnrepository" "http://mvnrepository.com/"]
View
9 src/ring/middleware/cookies.clj
@@ -108,8 +108,9 @@
to the :cookies key on the request."
[handler]
(fn [request]
- (let [request (assoc request :cookies (parse-cookies request))
- response (handler request)]
- (-> response
- set-cookies
+ (let [request (if (request :cookies)
+ request
+ (assoc request :cookies (parse-cookies request)))]
+ (-> (handler request)
+ (set-cookies)
(dissoc :cookies)))))
View
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)))))))
View
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 {}))})))
View
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)}))
View
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*)
+ {}))))
View
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)
+ {}))))
View
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

Please sign in to comment.