1- let log_src =
2- Logs.Src. create ~doc: " Opium middleware for cookie-based sessions" " opium.session"
3- ;;
1+ let log_src = Logs.Src. create ~doc: " middleware for cookie-based sessions" " opium.session"
42
53module Logs = (val Logs. src_log log_src : Logs.LOG )
64module Map = Map. Make (String )
75
86module Session = struct
9- type t = string Map .t
7+ type t =
8+ { data : string Map .t
9+ ; should_set_cookie : bool
10+ }
1011
11- let empty = Map. empty
12+ let create should_set_cookie = { data = Map. empty; should_set_cookie }
1213
1314 let of_yojson yojson =
1415 let open Yojson.Safe.Util in
1516 let session_list =
1617 try Some (yojson |> to_assoc |> List. map (fun (k , v ) -> k, to_string v)) with
1718 | _ -> None
1819 in
19- session_list |> Option. map List. to_seq |> Option. map Map. of_seq
20+ session_list
21+ |> Option. map List. to_seq
22+ |> Option. map Map. of_seq
23+ |> Option. map (fun data -> { data; should_set_cookie = false })
2024 ;;
2125
22- let to_yojson session =
26+ let to_yojson { data = session ; _ } =
2327 `Assoc (session |> Map. to_seq |> List. of_seq |> List. map (fun (k , v ) -> k, `String v))
2428 ;;
2529
@@ -31,15 +35,56 @@ module Session = struct
3135 let to_json session = session |> to_yojson |> Yojson.Safe. to_string
3236
3337 let to_sexp session =
34- session
38+ let open Sexplib0.Sexp_conv in
39+ let open Sexplib0.Sexp in
40+ let data =
41+ session.data
42+ |> Map. to_seq
43+ |> List. of_seq
44+ |> sexp_of_list (sexp_of_pair sexp_of_string sexp_of_string)
45+ in
46+ List
47+ [ List [ Atom " data" ; data ]
48+ ; List [ Atom " should_set_cookie" ; sexp_of_bool session.should_set_cookie ]
49+ ]
50+ ;;
51+ end
52+
53+ module SessionChange = struct
54+ type t = string option Map .t
55+
56+ let empty = Map. empty
57+
58+ let merge Session. { data = session ; should_set_cookie } t =
59+ let data =
60+ Map. merge
61+ (fun _ session change ->
62+ match session, change with
63+ | _ , Some (Some change ) -> Some change
64+ | _ , Some None -> None
65+ | Some session , None -> Some session
66+ | None , None -> None )
67+ session
68+ t
69+ in
70+ Session. { data; should_set_cookie }
71+ ;;
72+
73+ let to_sexp t =
74+ t
3575 |> Map. to_seq
3676 |> List. of_seq
37- |> Sexplib0.Sexp_conv. (sexp_of_list (sexp_of_pair sexp_of_string sexp_of_string))
77+ |> Sexplib0.Sexp_conv. (
78+ sexp_of_list (sexp_of_pair sexp_of_string (sexp_of_option sexp_of_string)))
3879 ;;
3980end
4081
4182module Env = struct
4283 let key : Session.t Context.key = Context.Key. create (" session" , Session. to_sexp)
84+
85+ let key_session_change : SessionChange.t Context.key =
86+ Context.Key. create (" session change" , SessionChange. to_sexp)
87+ ;;
4388end
4489
4590exception Session_not_found
@@ -52,66 +97,71 @@ let find key req =
5297 Logs. info (fun m -> m " Have you applied the session middleware?" );
5398 raise @@ Session_not_found
5499 in
55- Map. find_opt key session
100+ Map. find_opt key session.data
56101;;
57102
58103let set (key , value ) resp =
59- let session =
60- try Context. find_exn Env. key resp.Response. env with
61- | _ ->
62- Logs. err (fun m -> m " No session found" );
63- Logs. info (fun m -> m " Have you applied the session middleware?" );
64- raise Session_not_found
65- in
66- let updated_session =
67- match value with
68- | None -> Map. remove key session
69- | Some value -> Map. add key value session
104+ let change =
105+ match Context. find Env. key_session_change resp.Response. env with
106+ | Some change -> Map. add key value change
107+ | None -> SessionChange. empty |> Map. add key value
70108 in
71109 let env = resp.Response. env in
72- let env = Context. add Env. key updated_session env in
110+ let env = Context. add Env. key_session_change change env in
73111 { resp with env }
74112;;
75113
76- let persist_session signed_with cookie_key resp =
77- let session = Context. find Env. key resp.Response. env in
78- match session with
79- | None -> (* No need to touch the session cookie *) resp
80- | Some session ->
81- (* The session changed, we need to persist the new session in the cookie *)
82- let cookie_value = Session. to_json session in
83- let cookie = cookie_key, cookie_value in
84- let resp = Response. add_cookie_or_replace ~sign_with: signed_with cookie resp in
85- resp
114+ let decode_session cookie_key signed_with req =
115+ match Request. cookie ~signed_with cookie_key req with
116+ | None -> Session. create true
117+ | Some cookie_value ->
118+ (match Session. of_json cookie_value with
119+ | None ->
120+ Logs. err (fun m ->
121+ m
122+ " Failed to parse value found in session cookie '%s': '%s'"
123+ cookie_key
124+ cookie_value);
125+ Logs. info (fun m ->
126+ m
127+ " Maybe the cookie key '%s' collides with a cookie issued by someone else. \
128+ Try to change the cookie key."
129+ cookie_key);
130+ Session. create true
131+ | Some session -> session)
132+ ;;
133+
134+ let persist_session current_session signed_with cookie_key resp =
135+ let session_change = Context. find Env. key_session_change resp.Response. env in
136+ let cookie =
137+ match current_session.Session. should_set_cookie, session_change with
138+ | true , Some session_change ->
139+ let session = SessionChange. merge current_session session_change in
140+ let cookie_value = Session. to_json session in
141+ Some (cookie_key, cookie_value)
142+ | true , None ->
143+ let cookie_value = Session. to_json (Session. create true ) in
144+ Some (cookie_key, cookie_value)
145+ | false , Some session_change ->
146+ let session = SessionChange. merge current_session session_change in
147+ let cookie_value = Session. to_json session in
148+ Some (cookie_key, cookie_value)
149+ | false , None -> None
150+ in
151+ match cookie with
152+ | None -> resp
153+ | Some cookie -> Response. add_cookie_or_replace ~sign_with: signed_with cookie resp
86154;;
87155
88156let m ?(cookie_key = " _session" ) signed_with =
89157 let open Lwt.Syntax in
90158 let filter handler req =
91- let session =
92- match Request. cookie ~signed_with cookie_key req with
93- | None -> Session. empty
94- | Some cookie_value ->
95- (match Session. of_json cookie_value with
96- | None ->
97- Logs. err (fun m ->
98- m
99- " Failed to parse value found in session cookie '%s': '%s'"
100- cookie_key
101- cookie_value);
102- Logs. info (fun m ->
103- m
104- " Maybe the cookie key '%s' collides with a cookie issued by someone \
105- else. Try to change the cookie key."
106- cookie_key);
107- Session. empty
108- | Some session -> session)
109- in
159+ let session = decode_session cookie_key signed_with req in
110160 let env = req.Request. env in
111161 let env = Context. add Env. key session env in
112162 let req = { req with env } in
113163 let * resp = handler req in
114- Lwt. return @@ persist_session signed_with cookie_key resp
164+ Lwt. return @@ persist_session session signed_with cookie_key resp
115165 in
116- Rock.Middleware. create ~name: " Session " ~filter
166+ Rock.Middleware. create ~name: " session " ~filter
117167;;
0 commit comments