Skip to content

Commit 1ba5788

Browse files
author
Henrik Feldt
authored
Merge pull request #634 from JacobChang/cookie-samesite
Support samesite flag of cookie
2 parents 22679a3 + fa1327d commit 1ba5788

File tree

5 files changed

+72
-11
lines changed

5 files changed

+72
-11
lines changed

src/Suave.Tests/Cookie.fs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,36 @@ let parseResultCookie (_:SuaveConfig) =
2424
path = Some "/"
2525
domain = None
2626
secure = false
27-
httpOnly = true }
27+
httpOnly = true
28+
sameSite = None }
29+
Expect.equal subject expected "cookie should eq"
30+
31+
testCase "parse SameSite=Strict" <| fun _ ->
32+
let sample = @"st=oFqpYxbMObHvpEW!QLzedHwSZ1gZnotBs$; Path=/; HttpOnly; SameSite=Strict"
33+
let subject = Cookie.parseResultCookie sample
34+
let expected =
35+
{ name = "st"
36+
value = "oFqpYxbMObHvpEW!QLzedHwSZ1gZnotBs$"
37+
expires = None
38+
path = Some "/"
39+
domain = None
40+
secure = false
41+
httpOnly = true
42+
sameSite = Some Strict }
43+
Expect.equal subject expected "cookie should eq"
44+
45+
testCase "parse SameSite=Lax" <| fun _ ->
46+
let sample = @"st=oFqpYxbMObHvpEW!QLzedHwSZ1gZnotBs$; Path=/; HttpOnly; SameSite=Lax"
47+
let subject = Cookie.parseResultCookie sample
48+
let expected =
49+
{ name = "st"
50+
value = "oFqpYxbMObHvpEW!QLzedHwSZ1gZnotBs$"
51+
expires = None
52+
path = Some "/"
53+
domain = None
54+
secure = false
55+
httpOnly = true
56+
sameSite = Some Lax }
2857
Expect.equal subject expected "cookie should eq"
2958

3059
testCase "parse secure" <| fun _ ->
@@ -35,7 +64,8 @@ let parseResultCookie (_:SuaveConfig) =
3564
path = Some "/"
3665
domain = None
3766
secure = true
38-
httpOnly = false }
67+
httpOnly = false
68+
sameSite = None }
3969
let parsed = Cookie.parseResultCookie (HttpCookie.toHeader cookie)
4070
Expect.equal parsed cookie "eq"
4171

@@ -87,7 +117,8 @@ let setCookie (_ : SuaveConfig) =
87117
path = Some "/"
88118
domain = None
89119
secure = true
90-
httpOnly = false }
120+
httpOnly = false
121+
sameSite = None }
91122
let ctx = Cookie.setCookie cookie { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }}
92123
Expect.isTrue (List.isEmpty log.logs) "Should be no logs generated"
93124
testCase "set cookie - no warning when = 4k" <| fun _ ->
@@ -99,7 +130,8 @@ let setCookie (_ : SuaveConfig) =
99130
path = Some "/"
100131
domain = None
101132
secure = true
102-
httpOnly = false }
133+
httpOnly = false
134+
sameSite = None }
103135
let ctx = Cookie.setCookie cookie { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }}
104136
Expect.isTrue (List.isEmpty log.logs) "Should be no logs generated"
105137

@@ -112,7 +144,8 @@ let setCookie (_ : SuaveConfig) =
112144
path = Some "/"
113145
domain = None
114146
secure = true
115-
httpOnly = false }
147+
httpOnly = false
148+
sameSite = None }
116149
let ctx =
117150
let input = { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }}
118151
Cookie.setCookie cookie input |> Async.RunSynchronously

src/Suave.Tests/HttpWriters.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ let cookies cfg =
2727
domain = None
2828
path = Some "/"
2929
httpOnly = false
30-
secure = false }
30+
secure = false
31+
sameSite = None }
3132

3233
let ip, port =
3334
let binding = SuaveConfig.firstBinding cfg

src/Suave/Cookie.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ module Cookie =
4040
let parseResultCookie (s : string) : HttpCookie =
4141
let parseExpires (str : string) =
4242
DateTimeOffset.ParseExact(str, "R", CultureInfo.InvariantCulture)
43+
let parseSameSite (str : string) =
44+
match str with
45+
| "Strict" -> Some Strict
46+
| "Lax" -> Some Lax
47+
| _ -> None
4348
s.Split(';')
4449
|> Array.map (fun (x : string) ->
4550
let parts = x.Split('=')
@@ -55,6 +60,7 @@ module Cookie =
5560
| "Expires", expires -> iter + 1, { cookie with expires = Some (parseExpires expires) }
5661
| "HttpOnly", _ -> iter + 1, { cookie with httpOnly = true }
5762
| "Secure", _ -> iter + 1, { cookie with secure = true }
63+
| "SameSite", sameSite -> iter + 1, { cookie with sameSite = parseSameSite sameSite}
5864
| _ -> iter + 1, cookie)
5965
(0, { HttpCookie.empty with httpOnly = false }) // default when parsing
6066
|> snd

src/Suave/Http.fs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,19 @@ module Http =
216216
|> Array.map (fun case -> case.Name, FSharpValue.MakeUnion(case, [||]) :?> HttpCode)
217217
|> Map.ofArray
218218

219+
type SameSite =
220+
| Strict
221+
| Lax
222+
219223
type HttpCookie =
220224
{ name : string
221225
value : string
222226
expires : DateTimeOffset option
223227
path : string option
224228
domain : string option
225229
secure : bool
226-
httpOnly : bool }
230+
httpOnly : bool
231+
sameSite : SameSite option }
227232

228233
static member name_ = (fun x -> x.name), fun v (x : HttpCookie) -> { x with name = v }
229234
static member value_ = (fun x -> x.value), fun v (x : HttpCookie) -> { x with value = v }
@@ -232,18 +237,20 @@ module Http =
232237
static member domain_ = (fun x -> x.domain), fun v x -> { x with domain = v }
233238
static member secure_ = (fun x -> x.secure), fun v x -> { x with secure = v }
234239
static member httpOnly_ = (fun x -> x.httpOnly), fun v x -> { x with httpOnly = v }
240+
static member sameSite_ = (fun x -> x.sameSite), fun v x -> { x with sameSite = v }
235241

236242
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
237243
module HttpCookie =
238244

239-
let create name value expires path domain secure httpOnly =
245+
let create name value expires path domain secure httpOnly sameSite =
240246
{ name = name
241247
value = value
242248
expires = expires
243249
path = path
244250
domain = domain
245251
secure = secure
246-
httpOnly = httpOnly }
252+
httpOnly = httpOnly
253+
sameSite = sameSite }
247254

248255

249256
let createKV name value =
@@ -253,7 +260,8 @@ module Http =
253260
path = Some "/"
254261
domain = None
255262
secure = false
256-
httpOnly = true }
263+
httpOnly = true
264+
sameSite = None }
257265

258266
let empty = createKV "" ""
259267

@@ -267,6 +275,12 @@ module Http =
267275
x.expires |> appkv "Expires" (fun (i : DateTimeOffset) -> i.ToString("R"))
268276
if x.httpOnly then app "HttpOnly"
269277
if x.secure then app "Secure"
278+
match x.sameSite with
279+
| None -> ()
280+
| Some(sameSite) ->
281+
match sameSite with
282+
| Strict -> Some "Strict" |> appkv "SameSite" id
283+
| Lax -> Some "Lax" |> appkv "SameSite" id
270284
sb.ToString ()
271285

272286
type MimeType =

src/Suave/Http.fsi

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ module Http =
6060

6161
static member tryParse : code:int -> Choice<HttpCode, string>
6262

63+
type SameSite =
64+
| Strict
65+
| Lax
66+
6367
/// HTTP cookie
6468
type HttpCookie =
6569
{ name : string
@@ -71,7 +75,8 @@ module Http =
7175
/// This cookie is not forwarded over plaintext transports
7276
secure : bool
7377
/// This cookie is not readable from JavaScript
74-
httpOnly : bool }
78+
httpOnly : bool
79+
sameSite : SameSite option }
7580

7681
static member name_ : Property<HttpCookie, string>
7782
static member value_ : Property<HttpCookie, string>
@@ -80,6 +85,7 @@ module Http =
8085
static member domain_ : Property<HttpCookie, string option>
8186
static member secure_ : Property<HttpCookie, bool>
8287
static member httpOnly_ : Property<HttpCookie, bool>
88+
static member sameSite_ : Property<HttpCookie, SameSite option>
8389

8490
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
8591
module HttpCookie =
@@ -88,6 +94,7 @@ module Http =
8894
val create : name:string -> value:string -> expires:DateTimeOffset option
8995
-> path:string option -> domain:string option -> secure:bool
9096
-> httpOnly:bool
97+
-> sameSite:SameSite option
9198
-> HttpCookie
9299

93100
/// Create a new cookie with the given name, value, and defaults:

0 commit comments

Comments
 (0)