Current section

34 Versions

Jump to

Compare versions

4 files changed
+115 additions
-145 deletions
  @@ -3,9 +3,8 @@
3 3 {<<"description">>,<<"Types and functions for HTTP clients and servers!">>}.
4 4 {<<"files">>,
5 5 [<<"LICENSE">>,<<"README.md">>,<<"rebar.config">>,<<"rebar.lock">>,
6 - <<"src/gleam/http.gleam">>,<<"src/gleam/http/cookie.gleam">>,
7 - <<"src/gleam/http/middleware.gleam">>,<<"src/gleam_http.app.src">>,
8 - <<"src/gleam_http_native.erl">>]}.
6 + <<"src/gleam/http.gleam">>,<<"src/gleam/http/middleware.gleam">>,
7 + <<"src/gleam_http.app.src">>,<<"src/gleam_http_native.erl">>]}.
9 8 {<<"licenses">>,[<<"Apache 2.0">>]}.
10 9 {<<"links">>,[]}.
11 10 {<<"name">>,<<"gleam_http">>}.
  @@ -14,4 +13,4 @@
14 13 [{<<"app">>,<<"gleam_stdlib">>},
15 14 {<<"optional">>,false},
16 15 {<<"requirement">>,<<"~> 0.10.0">>}]}]}.
17 - {<<"version">>,<<"1.2.0">>}.
16 + {<<"version">>,<<"1.3.0">>}.
  @@ -11,6 +11,8 @@
11 11 // TODO: set_req_header
12 12 // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L776
13 13 import gleam/bit_string
14 + import gleam/dynamic.{Dynamic}
15 + import gleam/int
14 16 import gleam/list
15 17 import gleam/option.{None, Option, Some}
16 18 import gleam/regex
  @@ -18,8 +20,6 @@ import gleam/result
18 20 import gleam/string
19 21 import gleam/string_builder
20 22 import gleam/uri.{Uri}
21 - import gleam/dynamic.{Dynamic}
22 - import gleam/http/cookie
23 23
24 24 /// HTTP standard method as defined by [RFC 2616](https://tools.ietf.org/html/rfc2616),
25 25 /// and PATCH which is defined by [RFC 5789](https://tools.ietf.org/html/rfc5789).
  @@ -531,6 +531,25 @@ pub fn get_req_cookies(req) -> List(tuple(String, String)) {
531 531 |> list.flatten()
532 532 }
533 533
534 + const epoch = "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
535 +
536 + /// Policy options for the SameSite cookie attribute
537 + ///
538 + /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
539 + pub type SameSitePolicy {
540 + Lax
541 + Strict
542 + None
543 + }
544 +
545 + fn same_site_to_string(policy) {
546 + case policy {
547 + Lax -> "Lax"
548 + Strict -> "Strict"
549 + None -> "None"
550 + }
551 + }
552 +
534 553 /// Send a cookie with a request
535 554 ///
536 555 /// Multiple cookies are added to the same cookie header.
  @@ -570,20 +589,104 @@ pub fn set_req_cookie(req, name, value) {
570 589 )
571 590 }
572 591
592 + /// Attributes of a cookie when sent to a client in the `set-cookie` header.
593 + pub type CookieAttributes {
594 + CookieAttributes(
595 + max_age: Option(Int),
596 + domain: Option(String),
597 + path: Option(String),
598 + secure: Bool,
599 + http_only: Bool,
600 + same_site: Option(SameSitePolicy),
601 + )
602 + }
603 +
604 + /// Helper to create sensible default attributes for a set cookie.
605 + ///
606 + /// Note these defaults may not be sufficient to secure your application.
607 + /// You should consider setting the SameSite field.
608 + ///
609 + /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Attributes
610 + pub fn cookie_defaults(scheme: Scheme) {
611 + CookieAttributes(
612 + max_age: option.None,
613 + domain: option.None,
614 + path: Some("/"),
615 + secure: scheme == Https,
616 + http_only: True,
617 + same_site: option.None,
618 + )
619 + }
620 +
621 + fn cookie_attributes_to_list(attributes) {
622 + let CookieAttributes(
623 + max_age: max_age,
624 + domain: domain,
625 + path: path,
626 + secure: secure,
627 + http_only: http_only,
628 + same_site: same_site,
629 + ) = attributes
630 + [
631 + // Expires is a deprecated attribute for cookies, it has been replaced with MaxAge
632 + // MaxAge is widely supported and so Expires values are not set.
633 + // Only when deleting cookies is the exception made to use the old format,
634 + // to ensure complete clearup of cookies if required by an application.
635 + case max_age {
636 + Some(0) -> Some([epoch])
637 + _ -> option.None
638 + },
639 + option.map(max_age, fn(max_age) { ["MaxAge=", int.to_string(max_age)] }),
640 + option.map(domain, fn(domain) { ["Domain=", domain] }),
641 + option.map(path, fn(path) { ["Path=", path] }),
642 + case secure {
643 + True -> Some(["Secure"])
644 + False -> option.None
645 + },
646 + case http_only {
647 + True -> Some(["HttpOnly"])
648 + False -> option.None
649 + },
650 + option.map(
651 + same_site,
652 + fn(same_site) { ["SameSite=", same_site_to_string(same_site)] },
653 + ),
654 + ]
655 + |> list.filter_map(option.to_result(_, Nil))
656 + }
657 +
573 658 /// Set a cookie value for a client
574 659 ///
575 660 /// The attributes record is defined in `gleam/http/cookie`
576 661 pub fn set_resp_cookie(resp, name, value, attributes) {
577 - prepend_resp_header(
578 - resp,
579 - "set-cookie",
580 - cookie.set_cookie_string(name, value, attributes),
581 - )
662 + let header_value = [
663 + [name, "=", value],
664 + ..cookie_attributes_to_list(attributes)
665 + ]
666 + |> list.map(string.join(_, ""))
667 + |> string.join("; ")
668 + prepend_resp_header(resp, "set-cookie", header_value)
582 669 }
583 670
584 671 /// Expire a cookie value for a client
585 672 ///
586 673 /// Not the attributes value should be the same as when the response cookie was set.
587 674 pub fn expire_resp_cookie(resp, name, attributes) {
588 - set_resp_cookie(resp, name, "", cookie.expire_attributes(attributes))
675 + let CookieAttributes(
676 + max_age: _max_age,
677 + domain: domain,
678 + path: path,
679 + secure: secure,
680 + http_only: http_only,
681 + same_site: same_site,
682 + ) = attributes
683 + let attrs = CookieAttributes(
684 + max_age: Some(0),
685 + domain: domain,
686 + path: path,
687 + secure: secure,
688 + http_only: http_only,
689 + same_site: same_site,
690 + )
691 + set_resp_cookie(resp, name, "", attrs)
589 692 }
  @@ -1,132 +0,0 @@
1 - import gleam/int
2 - import gleam/list
3 - import gleam/option.{Option, Some}
4 - import gleam/string
5 -
6 - const epoch = "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
7 -
8 - /// Policy options for the SameSite cookie attribute
9 - ///
10 - /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
11 - pub type SameSitePolicy {
12 - Lax
13 - Strict
14 - None
15 - }
16 -
17 - /// Attributes of a cookie when sent to a client in the `set-cookie` header.
18 - pub type Attributes {
19 - Attributes(
20 - max_age: Option(Int),
21 - domain: Option(String),
22 - path: Option(String),
23 - secure: Bool,
24 - http_only: Bool,
25 - same_site: Option(SameSitePolicy),
26 - )
27 - }
28 -
29 - /// Helper to create empty `Attributes` for a cookie.
30 - pub fn empty_attributes() {
31 - Attributes(
32 - max_age: option.None,
33 - domain: option.None,
34 - path: option.None,
35 - secure: False,
36 - http_only: False,
37 - same_site: option.None,
38 - )
39 - }
40 -
41 - /// Helper to create sensible default attributes for a set cookie.
42 - ///
43 - /// NOTE these defaults ensure you cookie is always available to you application.
44 - /// However this is not a fully secure solution.
45 - /// You should consider setting a Secure and/or SameSite attribute.
46 - ///
47 - /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Attributes
48 - pub fn default_attributes() {
49 - Attributes(
50 - max_age: option.None,
51 - domain: option.None,
52 - path: Some("/"),
53 - secure: False,
54 - http_only: True,
55 - same_site: option.None,
56 - )
57 - }
58 -
59 - fn same_site_to_string(policy) {
60 - case policy {
61 - Lax -> "Lax"
62 - Strict -> "Strict"
63 - None -> "None"
64 - }
65 - }
66 -
67 - /// Update the MaxAge of a set of attributes to 0.
68 - /// This informes the client that the cookie should be expired.
69 - pub fn expire_attributes(attributes) {
70 - let Attributes(
71 - max_age: _max_age,
72 - domain: domain,
73 - path: path,
74 - secure: secure,
75 - http_only: http_only,
76 - same_site: same_site,
77 - ) = attributes
78 - Attributes(
79 - max_age: Some(0),
80 - domain: domain,
81 - path: path,
82 - secure: secure,
83 - http_only: http_only,
84 - same_site: same_site,
85 - )
86 - }
87 -
88 - fn attributes_to_list(attributes) {
89 - let Attributes(
90 - max_age: max_age,
91 - domain: domain,
92 - path: path,
93 - secure: secure,
94 - http_only: http_only,
95 - same_site: same_site,
96 - ) = attributes
97 - [
98 - // Expires is a deprecated attribute for cookies, it has been replaced with MaxAge
99 - // MaxAge is widely supported and so Expires values are not set.
100 - // Only when deleting cookies is the exception made to use the old format,
101 - // to ensure complete clearup of cookies if required by an application.
102 - case max_age {
103 - Some(0) -> Some([epoch])
104 - _ -> option.None
105 - },
106 - option.map(max_age, fn(max_age) { ["MaxAge=", int.to_string(max_age)] }),
107 - option.map(domain, fn(domain) { ["Domain=", domain] }),
108 - option.map(path, fn(path) { ["Path=", path] }),
109 - case secure {
110 - True -> Some(["Secure"])
111 - False -> option.None
112 - },
113 - case http_only {
114 - True -> Some(["HttpOnly"])
115 - False -> option.None
116 - },
117 - option.map(
118 - same_site,
119 - fn(same_site) { ["SameSite=", same_site_to_string(same_site)] },
120 - ),
121 - ]
122 - |> list.filter_map(option.to_result(_, Nil))
123 - }
124 -
125 - /// Serialize a cookie and attributes.
126 - ///
127 - /// This is the serialization of a cookie for the `set-cookie` header
128 - pub fn set_cookie_string(name, value, attributes) {
129 - [[name, "=", value], ..attributes_to_list(attributes)]
130 - |> list.map(string.join(_, ""))
131 - |> string.join("; ")
132 - }
  @@ -1,6 +1,6 @@
1 1 {application,gleam_http,
2 2 [{description,"Types and functions for HTTP clients and servers!"},
3 - {vsn,"1.2.0"},
3 + {vsn,"1.3.0"},
4 4 {registered,[]},
5 5 {applications,[kernel,stdlib]},
6 6 {env,[]},