Source file web_session.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
let log_src = Logs.Src.create "sihl.middleware.session"
module Logs = (val Logs.src_log log_src : Logs.LOG)
module Map = Map.Make (String)
module Session = struct
type t =
{ data : string Map.t
; should_set_cookie : bool
}
let create should_set_cookie = { data = Map.empty; should_set_cookie }
let of_yojson yojson =
let open Yojson.Safe.Util in
let session_list =
try
Some (yojson |> to_assoc |> List.map (fun (k, v) -> k, to_string v))
with
| _ -> None
in
session_list
|> Option.map List.to_seq
|> Option.map Map.of_seq
|> Option.map (fun data -> { data; should_set_cookie = false })
;;
let to_yojson { data = session; _ } =
`Assoc
(session
|> Map.to_seq
|> List.of_seq
|> List.map (fun (k, v) -> k, `String v))
;;
let of_json json =
try of_yojson (Yojson.Safe.from_string json) with
| _ -> None
;;
let to_json session = session |> to_yojson |> Yojson.Safe.to_string
let to_sexp session =
let open Sexplib0.Sexp_conv in
let open Sexplib0.Sexp in
let data =
session.data
|> Map.to_seq
|> List.of_seq
|> sexp_of_list (sexp_of_pair sexp_of_string sexp_of_string)
in
List
[ List [ Atom "data"; data ]
; List
[ Atom "should_set_cookie"; sexp_of_bool session.should_set_cookie ]
]
;;
end
module SessionChange = struct
type t = string option Map.t
let empty = Map.empty
let merge Session.{ data = session; should_set_cookie } t =
let data =
Map.merge
(fun _ session change ->
match session, change with
| _, Some (Some change) -> Some change
| _, Some None -> None
| Some session, None -> Some session
| None, None -> None)
session
t
in
Session.{ data; should_set_cookie }
;;
let to_sexp t =
t
|> Map.to_seq
|> List.of_seq
|> Sexplib0.Sexp_conv.(
sexp_of_list
(sexp_of_pair sexp_of_string (sexp_of_option sexp_of_string)))
;;
end
module Env = struct
let key : Session.t Opium.Context.key =
Opium.Context.Key.create ("session", Session.to_sexp)
;;
let key_session_change : SessionChange.t Opium.Context.key =
Opium.Context.Key.create ("session change", SessionChange.to_sexp)
;;
end
exception Session_not_found
let find key req =
let session =
try Opium.Context.find_exn Env.key req.Opium.Request.env with
| _ ->
Logs.err (fun m -> m "No session found");
Logs.info (fun m -> m "Have you applied the session middleware?");
raise @@ Session_not_found
in
Map.find_opt key session.data
;;
let set (key, value) resp =
let change =
match Opium.Context.find Env.key_session_change resp.Opium.Response.env with
| Some change -> Map.add key value change
| None -> SessionChange.empty |> Map.add key value
in
let env = resp.Opium.Response.env in
let env = Opium.Context.add Env.key_session_change change env in
{ resp with env }
;;
let decode_session cookie_key signed_with req =
match Opium.Request.cookie ~signed_with cookie_key req with
| None -> Session.create true
| Some cookie_value ->
(match Session.of_json cookie_value with
| None ->
Logs.err (fun m ->
m
"Failed to parse value found in session cookie '%s': '%s'"
cookie_key
cookie_value);
Logs.info (fun m ->
m
"Maybe the cookie key '%s' collides with a cookie issued by \
someone else. Try to change the cookie key."
cookie_key);
Session.create true
| Some session -> session)
;;
let persist_session current_session signed_with cookie_key resp =
let session_change =
Opium.Context.find Env.key_session_change resp.Opium.Response.env
in
let cookie =
match current_session.Session.should_set_cookie, session_change with
| true, Some session_change ->
let session = SessionChange.merge current_session session_change in
let cookie_value = Session.to_json session in
Some (cookie_key, cookie_value)
| true, None ->
let cookie_value = Session.to_json (Session.create true) in
Some (cookie_key, cookie_value)
| false, Some session_change ->
let session = SessionChange.merge current_session session_change in
let cookie_value = Session.to_json session in
Some (cookie_key, cookie_value)
| false, None -> None
in
match cookie with
| None -> resp
| Some cookie ->
Opium.Response.add_cookie_or_replace ~sign_with:signed_with cookie resp
;;
let middleware
?(cookie_key = "_session")
?(secret = Core_configuration.read_secret ())
()
=
let open Lwt.Syntax in
let filter handler req =
let signed_with = Opium.Cookie.Signer.make secret in
let session = decode_session cookie_key signed_with req in
let env = req.Opium.Request.env in
let env = Opium.Context.add Env.key session env in
let req = { req with env } in
let* resp = handler req in
Lwt.return @@ persist_session session signed_with cookie_key resp
in
Rock.Middleware.create ~name:"session" ~filter
;;