Current section
34 Versions
Jump to
Current section
34 Versions
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,[]}, |