Skip to content

Commit 379c757

Browse files
author
Josef Erben
committed
Add tests
1 parent 4ff7fcf commit 379c757

File tree

4 files changed

+328
-54
lines changed

4 files changed

+328
-54
lines changed
Lines changed: 103 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
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

53
module Logs = (val Logs.src_log log_src : Logs.LOG)
64
module Map = Map.Make (String)
75

86
module 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
;;
3980
end
4081

4182
module 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+
;;
4388
end
4489

4590
exception 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

58103
let 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

88156
let 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
;;

opium/src/opium.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ end
2323
module Session : sig
2424
exception Session_not_found
2525

26+
(** [find key req] returns the session value associated with the key [key] in the
27+
current session. *)
2628
val find : string -> Request.t -> string option
29+
30+
(** [set session resp] returns a response [resp] that has a session value [session]
31+
associated to it. Session is a tuple [(key, value)] where the value is optional. Use
32+
[None] for the value to remove session values. *)
2733
val set : string * string option -> Response.t -> Response.t
2834
end
2935

opium/test/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
(tests
2-
(names middleware_allow_cors request response route)
2+
(names middleware_allow_cors middleware_cookie_session request response route)
33
(libraries alcotest alcotest-lwt lwt opium)
44
(package opium))

0 commit comments

Comments
 (0)