cookie-0.18.0/.cargo_vcs_info.json0000644000000001360000000000100123730ustar { "git": { "sha1": "4e48da0d913c15a202cd69d73b5ec33763ab9e86" }, "path_in_vcs": "" }cookie-0.18.0/.github/workflows/ci.yml000064400000000000000000000023461046102023000157030ustar 00000000000000name: CI on: [push, pull_request] env: CARGO_TERM_COLOR: always MSRV: "1.56" jobs: test: name: "${{ matrix.os.name }} (${{ matrix.toolchain }})" continue-on-error: false runs-on: ${{ matrix.os.distro }} strategy: fail-fast: false matrix: os: - { name: Linux, distro: ubuntu-latest } - { name: Windows, distro: windows-latest } toolchain: [stable, beta, nightly] steps: - name: Checkout Sources uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - name: Run Tests run: "cargo update && ./scripts/test.sh" shell: bash msrv: name: Check MSRV runs-on: ubuntu-latest steps: - name: Checkout Sources uses: actions/checkout@v4 - name: Install Rust Nightly uses: dtolnay/rust-toolchain@nightly - name: Downgrade Dependencies to Minimal Versions run: cargo update -Z minimal-versions - name: Install MSRV (${{ env.MSRV }}) uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.MSRV }} - name: Run Tests run: ./scripts/test.sh cookie-0.18.0/.gitignore000064400000000000000000000000241046102023000131470ustar 00000000000000/target /Cargo.lock cookie-0.18.0/.travis.yml000064400000000000000000000002501046102023000132710ustar 00000000000000language: rust matrix: include: - rust: stable - rust: beta - rust: nightly script: - ./scripts/test.sh notifications: email: on_success: never cookie-0.18.0/CHANGELOG.md000064400000000000000000000204211046102023000127730ustar 00000000000000# Version 0.18 ## Version 0.18.0 (Oct 9, 2023) ### Breaking Changes * The MSRV is now 1.56. * `Cookie::value()` no longer trims surrounding double quotes. (89eddd) Use `Cookie::value_trimmed()` for the previous behavior. * Many methods now expect a `T: Into` in place of `Cookie`. (49ff7b) Functions and methods that previously accepted a `Cookie` now accept any `T: Into`. This particularly affects the `CookieJar` API, which now allows simpler addition and removal of cookies: * `jar.add(("foo", "bar"));` * `jar.add(Cookie::build(("foo", "bar")).path("/"));` * `jar.remove("foo");` * `jar.remove(Cookie::build("foo").path("/"));` * `CookieJar::force_remove()` now expects a `T: AsRef` in place of `&Cookie`. Force-removal never requires more information than a cookie's name. The API has been simplified to reflect this. * `CookieBuilder::finish()` was deprecated in favor of `CookieBuilder::build()`. This largely serves as a compile-time notice that calling `finish()` or `build()` is largely unnecessary given that `CookieBuilder` implements `Into`. * `Cookie::named()` was deprecated in favor of using `Cookie::build()` or `Cookie::from()`. * `Cookie::named("foo")` is equivalent to `Cookie::from("foo")`. * `Cookie::build("foo")` begins building a cookie equivalent to `Cookie::named("foo")`. ### New Features * Added `Cookie::value_trimmed()` and `Cookie::name_value_trimmed()`. These versions of `Cookie::value()` and `Cookie::name_value()`, respectively, trim a matching pair of surrounding double quotes from the cookie's value, if any are present. * String-like types, tuples of string-like types, and `CookieBuilder` implement `Into`. Implementations of `Into` for string-like types (`&str`, `String`, `Cow`), tuples of string-like types `(name: string, value: string)`, and `CookieBuilder` were added. The former implementations create a cookie with a name corresponding to the string and an empty value. The tuple implementation creates a cookie with the given name and value strings. The `CookieBuilder` implementation returns the built cookie. * `Key` implements `Debug`. To not leak sensitive information, the representation is simply `"Key"`. * `CookieBuilder` implements `Borrow{Mut}`, `As{Ref,Mut}`, `Display`. * Added `CookieBuilder::inner{_mut}()` to (mutably) borrow cookies being built. * Added `PrefixedJar` and `CookieJar::prefixed{_mut}()`, which implement the cookie prefixes HTTP draft. ## Version 0.18.0.rc.0 (Sep 27, 2023) See the entry above for 0.18.0. # Version 0.17 ## Version 0.17.0 (Jan 22, 2022) ### Breaking Changes * Cookie parsing no longer removes a `.` `Domain` prefix. `Cookie::domain()` now removes a `.` prefix before returning. As these changes are inverses, they are not likely observable. The change only affects manually set `domain` values via the `.domain()` builder method, the `set_domain()` setter method, or similar, which will now have a prefix of `.` removed when returned by `Cookie::domain()`. This results in more consistent treatment of `Domain` values. ### New Features * Added `Cookie::split_parse()` and `Cookie::split_parse_encoded()` methods. The methods split a `;`-joined cookie string and parse/decode the split values. They return a newly introduced iterator value of type `SplitCookies` over the parse results. ### General Changes and Fixes * Parsing fuzzers were introduced and run for 48 CPU hours without failure. * `base64` was updated to `0.21`. # Version 0.16 ## Version 0.16.2 (Dec 16, 2022) ### General Changes * `base64` was updated to `0.20`. ## Version 0.16.1 (Sep 25, 2022) ### Changes and Fixes * The `,`, `(`, and `)` are percent-encoded/decoded when encoding is used. * The `aes-gcm` dependency was updated to 0.10. ## Version 0.16.0 (Dec 28, 2021) ### Breaking Changes * The MSRV is now `1.53`, up from `1.41` in `0.15`. * `time` has been updated to `0.3` and is reexported from the crate root. ### General Changes * `rust-crypto` dependencies were updated to their latest versions. # Version 0.15 ## Version 0.15.1 (Jul 14, 2021) ### Changes and Fixes * A panic that could result from non-char boundary indexing was fixed. * Stale doc references to version `0.14` were updated. ## Version 0.15.0 (Feb 25, 2021) ### Breaking Changes * `Cookie::force_remove()` takes `&Cookie` instead of `Cookie`. * Child jar methods split into immutable and mutable versions (`Cookie::{private{_mut}, signed{_mut}}`). * `Cookie::encoded()` returns a new `Display` struct. * Dates with year `<= 99` are handled like Chrome: range `0..=68` maps to `2000..=2068`, `69..=99` to `1969..=1999`. * `Cookie::{set_}expires()` operates on a new `Expiration` enum. ### New Features * Added `Cookie::make_removal()` to manually create expired cookies. * Added `Cookie::stripped()` display variant to print only the `name` and `value` of a cookie. * `Key` implements a constant-time `PartialEq`. * Added `Key::master()` to retrieve the full 512-bit master key. * Added `PrivateJar::decrypt()` to manually decrypt an encrypted `Cookie`. * Added `SignedJar::verify()` to manually verify a signed `Cookie`. * `Cookie::expires()` returns an `Option` to allow distinguishing between unset and `None` expirations. * Added `Cookie::expires_datetime()` to retrieve the expiration as an `OffsetDateTime`. * Added `Cookie::unset_expires()` to unset expirations. ### General Changes and Fixes * MSRV is 1.41. # Version 0.14 ## Version 0.14.3 (Nov 5, 2020) ### Changes and Fixes * `rust-crypto` dependencies were updated to their latest versions. ## Version 0.14.2 (Jul 22, 2020) ### Changes and Fixes * Documentation now builds on the stable channel. * `rust-crypto` dependencies were updated to their latest versions. * Fixed 'interator' -> 'iterator' documentation typo. ## Version 0.14.1 (Jun 5, 2020) ### Changes and Fixes * Updated `base64` dependency to 0.12. * Updated minimum `time` dependency to correct version: 0.2.11. * Added `readme` key to `Cargo.toml`, updated `license` field. ## Version 0.14.0 (May 29, 2020) ### Breaking Changes * The `Key::from_master()` method was deprecated in favor of the more aptly named `Key::derive_from()`. * The deprecated `CookieJar::clear()` method was removed. ### New Features * Added `Key::from()` to create a `Key` structure from a full-length key. * Signed and private cookie jars can be individually enabled via the new `signed` and `private` features, respectively. * Key derivation via key expansion can be individually enabled via the new `key-expansion` feature. ### General Changes and Fixes * `ring` is no longer a dependency: `RustCrypto`-based cryptography is used in lieu of `ring`. Prior to their inclusion here, the `hmac` and `hkdf` crates were audited. * Quotes, if present, are stripped from cookie values when parsing. # Version 0.13 ## Version 0.13.3 (Feb 3, 2020) ### Changes * The `time` dependency was unpinned from `0.2.4`, allowing any `0.2.x` version of `time` where `x >= 6`. ## Version 0.13.2 (Jan 28, 2020) ### Changes * The `time` dependency was pinned to `0.2.4` due to upstream breaking changes in `0.2.5`. ## Version 0.13.1 (Jan 23, 2020) ### New Features * Added the `CookieJar::reset_delta()` method, which reverts all _delta_ changes to a `CookieJar`. ## Version 0.13.0 (Jan 21, 2020) ### Breaking Changes * `time` was updated from 0.1 to 0.2. * `ring` was updated from 0.14 to 0.16. * `SameSite::None` now writes `SameSite=None` to correspond with updated `SameSite` draft. `SameSite` can be unset by passing `None` to `Cookie::set_same_site()`. * `CookieBuilder` gained a lifetime: `CookieBuilder<'c>`. ### General Changes and Fixes * Added a CHANGELOG. * `expires`, `max_age`, `path`, and `domain` can be unset by passing `None` to the respective `Cookie::set_{field}()` method. * The "Expires" field is limited to a date-time of Dec 31, 9999, 23:59:59. * The `%` character is now properly encoded and decoded. * Constructor methods on `CookieBuilder` allow non-static lifetimes. cookie-0.18.0/Cargo.toml0000644000000036020000000000100103720ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "cookie" version = "0.18.0" authors = [ "Sergio Benitez ", "Alex Crichton ", ] build = "build.rs" description = """ HTTP cookie parsing and cookie jar management. Supports signed and private (encrypted, authenticated) jars. """ documentation = "https://docs.rs/cookie" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/SergioBenitez/cookie-rs" [package.metadata.docs.rs] all-features = true [dependencies.aes-gcm] version = "0.10.0" optional = true [dependencies.base64] version = "0.21.4" optional = true [dependencies.hkdf] version = "0.12.0" optional = true [dependencies.hmac] version = "0.12.0" optional = true [dependencies.percent-encoding] version = "2.0" optional = true [dependencies.rand] version = "0.8" optional = true [dependencies.sha2] version = "0.10.0" optional = true [dependencies.subtle] version = "2.3" optional = true [dependencies.time] version = "0.3" features = [ "std", "parsing", "formatting", "macros", ] default-features = false [build-dependencies.version_check] version = "0.9.4" [features] key-expansion = [ "sha2", "hkdf", ] percent-encode = ["percent-encoding"] private = [ "aes-gcm", "base64", "rand", "subtle", ] secure = [ "private", "signed", "key-expansion", ] signed = [ "hmac", "sha2", "base64", "rand", "subtle", ] cookie-0.18.0/Cargo.toml.orig000064400000000000000000000024771046102023000140640ustar 00000000000000[package] name = "cookie" version = "0.18.0" authors = ["Sergio Benitez ", "Alex Crichton "] edition = "2018" license = "MIT OR Apache-2.0" repository = "https://github.com/SergioBenitez/cookie-rs" documentation = "https://docs.rs/cookie" readme = "README.md" build = "build.rs" description = """ HTTP cookie parsing and cookie jar management. Supports signed and private (encrypted, authenticated) jars. """ [features] percent-encode = ["percent-encoding"] secure = ["private", "signed", "key-expansion"] private = ["aes-gcm", "base64", "rand", "subtle"] signed = ["hmac", "sha2", "base64", "rand", "subtle"] key-expansion = ["sha2", "hkdf"] [dependencies] time = { version = "0.3", default-features = false, features = ["std", "parsing", "formatting", "macros"] } percent-encoding = { version = "2.0", optional = true } # dependencies for secure (private/signed) functionality aes-gcm = { version = "0.10.0", optional = true } hmac = { version = "0.12.0", optional = true } sha2 = { version = "0.10.0", optional = true } base64 = { version = "0.21.4", optional = true } rand = { version = "0.8", optional = true } hkdf = { version = "0.12.0", optional = true } subtle = { version = "2.3", optional = true } [build-dependencies] version_check = "0.9.4" [package.metadata.docs.rs] all-features = true cookie-0.18.0/LICENSE-APACHE000064400000000000000000000251571046102023000131210ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 Sergio Benitez Copyright 2014 Alex Chricton Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cookie-0.18.0/LICENSE-MIT000064400000000000000000000021031046102023000126130ustar 00000000000000Copyright (c) 2017 Sergio Benitez Copyright (c) 2014 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cookie-0.18.0/README.md000064400000000000000000000025751046102023000124530ustar 00000000000000# Cookie [![CI Status](https://github.com/SergioBenitez/cookie-rs/workflows/CI/badge.svg)](https://github.com/SergioBenitez/cookie-rs/actions) [![Current Crates.io Version](https://img.shields.io/crates/v/cookie.svg)](https://crates.io/crates/cookie) [![Documentation](https://docs.rs/cookie/badge.svg)](https://docs.rs/cookie) A Rust library for parsing HTTP cookies and managing cookie jars. # Usage Add the following to your `Cargo.toml`: ```toml [dependencies] cookie = "0.18" ``` See the [documentation](http://docs.rs/cookie) for detailed usage information. # Minimum Supported Rust Version (MSRV) | Version Range | MSRV | |----------------------------------|--------------| | `cookie` `==` `0.18` | `rustc 1.56` | | `0.16` `<=` `cookie` `<=` `0.17` | `rustc 1.53` | | `cookie` `<=` `0.15` | `rustc 1.41` | # License This project is licensed under your option of either of the following: * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in `cookie` by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. cookie-0.18.0/build.rs000064400000000000000000000002061046102023000126260ustar 00000000000000fn main() { if let Some(true) = version_check::supports_feature("doc_cfg") { println!("cargo:rustc-cfg=nightly"); } } cookie-0.18.0/scripts/test.sh000075500000000000000000000010011046102023000141600ustar 00000000000000#!/bin/bash set -e cargo build --verbose cargo test --verbose --features percent-encode cargo test --verbose --features private cargo test --verbose --features signed cargo test --verbose --features secure cargo test --verbose --features 'private,key-expansion' cargo test --verbose --features 'signed,key-expansion' cargo test --verbose --features 'secure,percent-encode' cargo test --verbose cargo test --verbose --no-default-features cargo test --verbose --all-features rustdoc --test README.md -L target cookie-0.18.0/src/builder.rs000064400000000000000000000242101046102023000137450ustar 00000000000000use std::borrow::{Cow, Borrow, BorrowMut}; use crate::{Cookie, SameSite, Expiration}; /// Structure that follows the builder pattern for building `Cookie` structs. /// /// To construct a cookie: /// /// 1. Call [`Cookie::build()`] to start building. /// 2. Use any of the builder methods to set fields in the cookie. /// /// The resulting `CookieBuilder` can be passed directly into methods expecting /// a `T: Into`: /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// /// let mut jar = CookieJar::new(); /// jar.add(Cookie::build(("key", "value")).secure(true).path("/")); /// jar.remove(Cookie::build("key").path("/")); /// ``` /// /// You can also call [`CookieBuilder::build()`] directly to get a `Cookie`: /// /// ```rust /// use cookie::Cookie; /// use cookie::time::Duration; /// /// let cookie: Cookie = Cookie::build(("name", "value")) /// .domain("www.rust-lang.org") /// .path("/") /// .secure(true) /// .http_only(true) /// .max_age(Duration::days(1)) /// .build(); /// ``` #[derive(Debug, Clone, PartialEq)] pub struct CookieBuilder<'c> { /// The cookie being built. cookie: Cookie<'c>, } impl<'c> CookieBuilder<'c> { /// Creates a new `CookieBuilder` instance from the given name and value. /// /// This method is typically called indirectly via [`Cookie::build()`]. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// // These two snippets are equivalent: /// /// let c = Cookie::build(("foo", "bar")); /// assert_eq!(c.inner().name_value(), ("foo", "bar")); /// /// let c = Cookie::new("foo", "bar"); /// assert_eq!(c.name_value(), ("foo", "bar")); /// ``` pub fn new(name: N, value: V) -> Self where N: Into>, V: Into> { CookieBuilder { cookie: Cookie::new(name, value) } } /// Sets the `expires` field in the cookie being built. /// /// See [`Expiration`] for conversions. /// /// # Example /// /// ```rust /// # extern crate cookie; /// use cookie::{Cookie, Expiration}; /// use cookie::time::OffsetDateTime; /// /// # fn main() { /// let c = Cookie::build(("foo", "bar")).expires(OffsetDateTime::now_utc()); /// assert!(c.inner().expires().is_some()); /// /// let c = Cookie::build(("foo", "bar")).expires(None); /// assert_eq!(c.inner().expires(), Some(Expiration::Session)); /// # } /// ``` #[inline] pub fn expires>(mut self, when: E) -> Self { self.cookie.set_expires(when); self } /// Sets the `max_age` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// use cookie::time::Duration; /// /// let c = Cookie::build(("foo", "bar")).max_age(Duration::minutes(30)); /// assert_eq!(c.inner().max_age(), Some(Duration::seconds(30 * 60))); /// ``` #[inline] pub fn max_age(mut self, value: time::Duration) -> Self { self.cookie.set_max_age(value); self } /// Sets the `domain` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let c = Cookie::build(("foo", "bar")).domain("www.rust-lang.org"); /// assert_eq!(c.inner().domain(), Some("www.rust-lang.org")); /// ``` pub fn domain>>(mut self, value: D) -> Self { self.cookie.set_domain(value); self } /// Sets the `path` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let c = Cookie::build(("foo", "bar")).path("/"); /// assert_eq!(c.inner().path(), Some("/")); /// ``` pub fn path>>(mut self, path: P) -> Self { self.cookie.set_path(path); self } /// Sets the `secure` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let c = Cookie::build(("foo", "bar")).secure(true); /// assert_eq!(c.inner().secure(), Some(true)); /// ``` #[inline] pub fn secure(mut self, value: bool) -> Self { self.cookie.set_secure(value); self } /// Sets the `http_only` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let c = Cookie::build(("foo", "bar")).http_only(true); /// assert_eq!(c.inner().http_only(), Some(true)); /// ``` #[inline] pub fn http_only(mut self, value: bool) -> Self { self.cookie.set_http_only(value); self } /// Sets the `same_site` field in the cookie being built. /// /// # Example /// /// ```rust /// use cookie::{Cookie, SameSite}; /// /// let c = Cookie::build(("foo", "bar")).same_site(SameSite::Strict); /// assert_eq!(c.inner().same_site(), Some(SameSite::Strict)); /// ``` #[inline] pub fn same_site(mut self, value: SameSite) -> Self { self.cookie.set_same_site(value); self } /// Makes the cookie being built 'permanent' by extending its expiration and /// max age 20 years into the future. /// /// # Example /// /// ```rust /// # extern crate cookie; /// use cookie::Cookie; /// use cookie::time::Duration; /// /// # fn main() { /// let c = Cookie::build(("foo", "bar")).permanent(); /// assert_eq!(c.inner().max_age(), Some(Duration::days(365 * 20))); /// # assert!(c.inner().expires().is_some()); /// # } /// ``` #[inline] pub fn permanent(mut self) -> Self { self.cookie.make_permanent(); self } /// Returns a borrow to the cookie currently being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let builder = Cookie::build(("name", "value")) /// .domain("www.rust-lang.org") /// .path("/") /// .http_only(true); /// /// assert_eq!(builder.inner().name_value(), ("name", "value")); /// assert_eq!(builder.inner().domain(), Some("www.rust-lang.org")); /// assert_eq!(builder.inner().path(), Some("/")); /// assert_eq!(builder.inner().http_only(), Some(true)); /// assert_eq!(builder.inner().secure(), None); /// ``` #[inline] pub fn inner(&self) -> &Cookie<'c> { &self.cookie } /// Returns a mutable borrow to the cookie currently being built. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let mut builder = Cookie::build(("name", "value")) /// .domain("www.rust-lang.org") /// .path("/") /// .http_only(true); /// /// assert_eq!(builder.inner().http_only(), Some(true)); /// /// builder.inner_mut().set_http_only(false); /// assert_eq!(builder.inner().http_only(), Some(false)); /// ``` #[inline] pub fn inner_mut(&mut self) -> &mut Cookie<'c> { &mut self.cookie } /// Finishes building and returns the built `Cookie`. /// /// This method usually does not need to be called directly. This is because /// `CookieBuilder` implements `Into`, so a value of `CookieBuilder` /// can be passed directly into any method that expects a `C: Into`. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// /// // We don't usually need to use `build()`. Inspect with `inner()`, and /// // pass the builder directly into methods expecting `T: Into`. /// let c = Cookie::build(("foo", "bar")) /// .domain("crates.io") /// .path("/"); /// /// // Use `inner()` and inspect the cookie. /// assert_eq!(c.inner().name_value(), ("foo", "bar")); /// assert_eq!(c.inner().domain(), Some("crates.io")); /// assert_eq!(c.inner().path(), Some("/")); /// /// // Add the cookie to a jar. Note the automatic conversion. /// CookieJar::new().add(c); /// /// // We could use `build()` to get a `Cookie` when needed. /// let c = Cookie::build(("foo", "bar")) /// .domain("crates.io") /// .path("/") /// .build(); /// /// // Inspect the built cookie. /// assert_eq!(c.name_value(), ("foo", "bar")); /// assert_eq!(c.domain(), Some("crates.io")); /// assert_eq!(c.path(), Some("/")); /// /// // Add the cookie to a jar. /// CookieJar::new().add(c); /// ``` #[inline] pub fn build(self) -> Cookie<'c> { self.cookie } /// Deprecated. Convert `self` into a `Cookie`. /// /// Instead of using this method, pass a `CookieBuilder` directly into /// methods expecting a `T: Into`. For other cases, use /// [`CookieBuilder::build()`]. #[deprecated(since="0.18.0", note="`CookieBuilder` can be passed in to methods expecting a `Cookie`; for other cases, use `CookieBuilder::build()`")] pub fn finish(self) -> Cookie<'c> { self.cookie } } impl std::fmt::Display for CookieBuilder<'_> { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.cookie.fmt(f) } } // NOTE: We don't implement `Deref` or `DerefMut` because there are tons of name // collisions with builder methods. impl<'a> Borrow> for CookieBuilder<'a> { fn borrow(&self) -> &Cookie<'a> { &self.cookie } } impl<'a> BorrowMut> for CookieBuilder<'a> { fn borrow_mut(&mut self) -> &mut Cookie<'a> { &mut self.cookie } } impl<'a> AsRef> for CookieBuilder<'a> { fn as_ref(&self) -> &Cookie<'a> { &self.cookie } } impl<'a> AsMut> for CookieBuilder<'a> { fn as_mut(&mut self) -> &mut Cookie<'a> { &mut self.cookie } } impl<'a, 'b> PartialEq> for CookieBuilder<'a> { fn eq(&self, other: &Cookie<'b>) -> bool { &self.cookie == other } } impl<'a, 'b> PartialEq> for Cookie<'a> { fn eq(&self, other: &CookieBuilder<'b>) -> bool { self == &other.cookie } } impl<'c> From> for CookieBuilder<'c> { fn from(cookie: Cookie<'c>) -> Self { CookieBuilder { cookie } } } cookie-0.18.0/src/delta.rs000064400000000000000000000031361046102023000134140ustar 00000000000000use std::ops::{Deref, DerefMut}; use std::hash::{Hash, Hasher}; use std::borrow::Borrow; use crate::Cookie; /// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a /// `Cookie` so that it can be hashed and compared purely by name. It further /// records whether the wrapped cookie is a "removal" cookie, that is, a cookie /// that when sent to the client removes the named cookie on the client's /// machine. #[derive(Clone, Debug)] pub(crate) struct DeltaCookie { pub cookie: Cookie<'static>, pub removed: bool, } impl DeltaCookie { /// Create a new `DeltaCookie` that is being added to a jar. #[inline] pub fn added(cookie: Cookie<'static>) -> DeltaCookie { DeltaCookie { cookie, removed: false, } } /// Create a new `DeltaCookie` that is being removed from a jar. The /// `cookie` should be a "removal" cookie. #[inline] pub fn removed(cookie: Cookie<'static>) -> DeltaCookie { DeltaCookie { cookie, removed: true, } } } impl Deref for DeltaCookie { type Target = Cookie<'static>; fn deref(&self) -> &Cookie<'static> { &self.cookie } } impl DerefMut for DeltaCookie { fn deref_mut(&mut self) -> &mut Cookie<'static> { &mut self.cookie } } impl PartialEq for DeltaCookie { fn eq(&self, other: &DeltaCookie) -> bool { self.name() == other.name() } } impl Eq for DeltaCookie {} impl Hash for DeltaCookie { fn hash(&self, state: &mut H) { self.name().hash(state); } } impl Borrow for DeltaCookie { fn borrow(&self) -> &str { self.name() } } cookie-0.18.0/src/expiration.rs000064400000000000000000000077501046102023000145130ustar 00000000000000use time::OffsetDateTime; /// A cookie's expiration: either a date-time or session. /// /// An `Expiration` is constructible with `Expiration::from()` via any of: /// /// * `None` -> `Expiration::Session` /// * `Some(OffsetDateTime)` -> `Expiration::DateTime` /// * `OffsetDateTime` -> `Expiration::DateTime` /// /// ```rust /// use cookie::Expiration; /// use time::OffsetDateTime; /// /// let expires = Expiration::from(None); /// assert_eq!(expires, Expiration::Session); /// /// let now = OffsetDateTime::now_utc(); /// let expires = Expiration::from(now); /// assert_eq!(expires, Expiration::DateTime(now)); /// /// let expires = Expiration::from(Some(now)); /// assert_eq!(expires, Expiration::DateTime(now)); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Expiration { /// Expiration for a "permanent" cookie at a specific date-time. DateTime(OffsetDateTime), /// Expiration for a "session" cookie. Browsers define the notion of a /// "session" and will automatically expire session cookies when they deem /// the "session" to be over. This is typically, but need not be, when the /// browser is closed. Session, } impl Expiration { /// Returns `true` if `self` is an `Expiration::DateTime`. /// /// # Example /// /// ```rust /// use cookie::Expiration; /// use time::OffsetDateTime; /// /// let expires = Expiration::from(None); /// assert!(!expires.is_datetime()); /// /// let expires = Expiration::from(OffsetDateTime::now_utc()); /// assert!(expires.is_datetime()); /// ``` pub fn is_datetime(&self) -> bool { match self { Expiration::DateTime(_) => true, Expiration::Session => false } } /// Returns `true` if `self` is an `Expiration::Session`. /// /// # Example /// /// ```rust /// use cookie::Expiration; /// use time::OffsetDateTime; /// /// let expires = Expiration::from(None); /// assert!(expires.is_session()); /// /// let expires = Expiration::from(OffsetDateTime::now_utc()); /// assert!(!expires.is_session()); /// ``` pub fn is_session(&self) -> bool { match self { Expiration::DateTime(_) => false, Expiration::Session => true } } /// Returns the inner `OffsetDateTime` if `self` is a `DateTime`. /// /// # Example /// /// ```rust /// use cookie::Expiration; /// use time::OffsetDateTime; /// /// let expires = Expiration::from(None); /// assert!(expires.datetime().is_none()); /// /// let now = OffsetDateTime::now_utc(); /// let expires = Expiration::from(now); /// assert_eq!(expires.datetime(), Some(now)); /// ``` pub fn datetime(self) -> Option { match self { Expiration::Session => None, Expiration::DateTime(v) => Some(v) } } /// Applied `f` to the inner `OffsetDateTime` if `self` is a `DateTime` and /// returns the mapped `Expiration`. /// /// # Example /// /// ```rust /// use cookie::Expiration; /// use time::{OffsetDateTime, Duration}; /// /// let now = OffsetDateTime::now_utc(); /// let one_week = Duration::weeks(1); /// /// let expires = Expiration::from(now); /// assert_eq!(expires.map(|t| t + one_week).datetime(), Some(now + one_week)); /// /// let expires = Expiration::from(None); /// assert_eq!(expires.map(|t| t + one_week).datetime(), None); /// ``` pub fn map(self, f: F) -> Self where F: FnOnce(OffsetDateTime) -> OffsetDateTime { match self { Expiration::Session => Expiration::Session, Expiration::DateTime(v) => Expiration::DateTime(f(v)), } } } impl>> From for Expiration { fn from(option: T) -> Self { match option.into() { Some(value) => Expiration::DateTime(value), None => Expiration::Session } } } cookie-0.18.0/src/jar.rs000064400000000000000000000670161046102023000131060ustar 00000000000000use std::collections::HashSet; #[cfg(feature = "signed")] use crate::secure::SignedJar; #[cfg(feature = "private")] use crate::secure::PrivateJar; #[cfg(any(feature = "signed", feature = "private"))] use crate::secure::Key; use crate::delta::DeltaCookie; use crate::prefix::{Prefix, PrefixedJar}; use crate::Cookie; /// A collection of cookies that tracks its modifications. /// /// A `CookieJar` provides storage for any number of cookies. Any changes made /// to the jar are tracked; the changes can be retrieved via the /// [`delta`](#method.delta) method which returns an iterator over the changes. /// /// # Usage /// /// A jar's life begins via [`CookieJar::new()`] and calls to /// [`add_original()`](#method.add_original): /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// /// let mut jar = CookieJar::new(); /// jar.add_original(("name", "value")); /// jar.add_original(("second", "another")); /// jar.add_original(Cookie::build(("third", "again")).path("/")); /// ``` /// /// Cookies can be added via [`CookieJar::add()`] and removed via /// [`CookieJar::remove()`]. Note that any `T: Into` can be passed into /// these methods; see [`Cookie::build()`] for a table of implementing types. /// /// Finally, cookies can be retrieved with [`CookieJar::get()`]. /// /// ```rust /// # use cookie::{Cookie, CookieJar}; /// let mut jar = CookieJar::new(); /// jar.add(("a", "one")); /// jar.add(("b", "two")); /// /// assert_eq!(jar.get("a").map(|c| c.value()), Some("one")); /// assert_eq!(jar.get("b").map(|c| c.value()), Some("two")); /// /// jar.remove("b"); /// assert!(jar.get("b").is_none()); /// ``` /// /// # Deltas /// /// A jar keeps track of any modifications made to it over time. The /// modifications are recorded as cookies. The modifications can be retrieved /// via [delta](#method.delta). Any new `Cookie` added to a jar via `add` /// results in the same `Cookie` appearing in the `delta`; cookies added via /// `add_original` do not count towards the delta. Any _original_ cookie that is /// removed from a jar results in a "removal" cookie appearing in the delta. A /// "removal" cookie is a cookie that a server sends so that the cookie is /// removed from the client's machine. /// /// Deltas are typically used to create `Set-Cookie` headers corresponding to /// the changes made to a cookie jar over a period of time. /// /// ```rust /// # use cookie::{Cookie, CookieJar}; /// let mut jar = CookieJar::new(); /// /// // original cookies don't affect the delta /// jar.add_original(("original", "value")); /// assert_eq!(jar.delta().count(), 0); /// /// // new cookies result in an equivalent `Cookie` in the delta /// jar.add(("a", "one")); /// jar.add(("b", "two")); /// assert_eq!(jar.delta().count(), 2); /// /// // removing an original cookie adds a "removal" cookie to the delta /// jar.remove("original"); /// assert_eq!(jar.delta().count(), 3); /// /// // removing a new cookie that was added removes that `Cookie` from the delta /// jar.remove("a"); /// assert_eq!(jar.delta().count(), 2); /// ``` #[derive(Default, Debug, Clone)] pub struct CookieJar { original_cookies: HashSet, delta_cookies: HashSet, } impl CookieJar { /// Creates an empty cookie jar. /// /// # Example /// /// ```rust /// use cookie::CookieJar; /// /// let jar = CookieJar::new(); /// assert_eq!(jar.iter().count(), 0); /// ``` pub fn new() -> CookieJar { CookieJar::default() } /// Returns a reference to the `Cookie` inside this jar with the name /// `name`. If no such cookie exists, returns `None`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// assert!(jar.get("name").is_none()); /// /// jar.add(("name", "value")); /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); /// ``` pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { self.delta_cookies .get(name) .or_else(|| self.original_cookies.get(name)) .and_then(|c| if c.removed { None } else { Some(&c.cookie) }) } /// Adds an "original" `cookie` to this jar. If an original cookie with the /// same name already exists, it is replaced with `cookie`. Cookies added /// with `add` take precedence and are not replaced by this method. /// /// Adding an original cookie does not affect the [delta](#method.delta) /// computation. This method is intended to be used to seed the cookie jar /// with cookies received from a client's HTTP message. /// /// For accurate `delta` computations, this method should not be called /// after calling `remove`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// jar.add_original(("name", "value")); /// jar.add_original(("second", "two")); /// /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); /// assert_eq!(jar.iter().count(), 2); /// assert_eq!(jar.delta().count(), 0); /// ``` pub fn add_original>>(&mut self, cookie: C) { self.original_cookies.replace(DeltaCookie::added(cookie.into())); } /// Adds `cookie` to this jar. If a cookie with the same name already /// exists, it is replaced with `cookie`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// jar.add(("name", "value")); /// jar.add(("second", "two")); /// /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); /// assert_eq!(jar.iter().count(), 2); /// assert_eq!(jar.delta().count(), 2); /// ``` pub fn add>>(&mut self, cookie: C) { self.delta_cookies.replace(DeltaCookie::added(cookie.into())); } /// Removes `cookie` from this jar. If an _original_ cookie with the same /// name as `cookie` is present in the jar, a _removal_ cookie will be /// present in the `delta` computation. **To properly generate the removal /// cookie, `cookie` must contain the same `path` and `domain` as the cookie /// that was initially set.** /// /// A "removal" cookie is a cookie that has the same name as the original /// cookie but has an empty value, a max-age of 0, and an expiration date /// far in the past. See also [`Cookie::make_removal()`]. /// /// # Example /// /// Removing an _original_ cookie results in a _removal_ cookie: /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// use cookie::time::Duration; /// /// let mut jar = CookieJar::new(); /// /// // Assume this cookie originally had a path of "/" and domain of "a.b". /// jar.add_original(("name", "value")); /// /// // If the path and domain were set, they must be provided to `remove`. /// jar.remove(Cookie::build("name").path("/").domain("a.b")); /// /// // The delta will contain the removal cookie. /// let delta: Vec<_> = jar.delta().collect(); /// assert_eq!(delta.len(), 1); /// assert_eq!(delta[0].name(), "name"); /// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0))); /// ``` /// /// Removing a new cookie does not result in a _removal_ cookie unless /// there's an original cookie with the same name: /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// jar.add(("name", "value")); /// assert_eq!(jar.delta().count(), 1); /// /// jar.remove("name"); /// assert_eq!(jar.delta().count(), 0); /// /// jar.add_original(("name", "value")); /// jar.add(("name", "value")); /// assert_eq!(jar.delta().count(), 1); /// /// jar.remove("name"); /// assert_eq!(jar.delta().count(), 1); /// ``` pub fn remove>>(&mut self, cookie: C) { let mut cookie = cookie.into(); if self.original_cookies.contains(cookie.name()) { cookie.make_removal(); self.delta_cookies.replace(DeltaCookie::removed(cookie)); } else { self.delta_cookies.remove(cookie.name()); } } /// Removes `cookie` from this jar completely. /// /// This method differs from `remove` in that no delta cookie is created /// under any condition. Thus, no path or domain are needed: only the /// cookie's name. Neither the `delta` nor `iter` methods will return a /// cookie that is removed using this method. /// /// # Example /// /// Removing an _original_ cookie; no _removal_ cookie is generated: /// /// ```rust /// # extern crate cookie; /// use cookie::{CookieJar, Cookie}; /// use cookie::time::Duration; /// /// # fn main() { /// let mut jar = CookieJar::new(); /// /// // Add an original cookie and a new cookie. /// jar.add_original(("name", "value")); /// jar.add(("key", "value")); /// assert_eq!(jar.delta().count(), 1); /// assert_eq!(jar.iter().count(), 2); /// /// // Now force remove the original cookie. /// jar.force_remove("name"); /// assert_eq!(jar.delta().count(), 1); /// assert_eq!(jar.iter().count(), 1); /// /// // Now force remove the new cookie. `to_string()` for illustration only. /// jar.force_remove("key".to_string()); /// assert_eq!(jar.delta().count(), 0); /// assert_eq!(jar.iter().count(), 0); /// # } /// ``` pub fn force_remove>(&mut self, name: N) { self.original_cookies.remove(name.as_ref()); self.delta_cookies.remove(name.as_ref()); } /// Removes all delta cookies, i.e. all cookies not added via /// [`CookieJar::add_original()`], from this `CookieJar`. This undoes any /// changes from [`CookieJar::add()`] and [`CookieJar::remove()`] /// operations. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// /// // Only original cookies will remain after calling `reset_delta`. /// jar.add_original(("name", "value")); /// jar.add_original(("language", "Rust")); /// /// // These operations, represented by delta cookies, will be reset. /// jar.add(("language", "C++")); /// jar.remove("name"); /// /// // All is normal. /// assert_eq!(jar.get("name"), None); /// assert_eq!(jar.get("language").map(Cookie::value), Some("C++")); /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 2); /// /// // Resetting undoes delta operations. /// jar.reset_delta(); /// assert_eq!(jar.get("name").map(Cookie::value), Some("value")); /// assert_eq!(jar.get("language").map(Cookie::value), Some("Rust")); /// assert_eq!(jar.iter().count(), 2); /// assert_eq!(jar.delta().count(), 0); /// ``` pub fn reset_delta(&mut self) { self.delta_cookies = HashSet::new(); } /// Returns an iterator over cookies that represent the changes to this jar /// over time. These cookies can be rendered directly as `Set-Cookie` header /// values to affect the changes made to this jar on the client. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// jar.add_original(("name", "value")); /// jar.add_original(("second", "two")); /// /// // Add new cookies. /// jar.add(("new", "third")); /// jar.add(("another", "fourth")); /// jar.add(("yac", "fifth")); /// /// // Remove some cookies. /// jar.remove(("name")); /// jar.remove(("another")); /// /// // Delta contains two new cookies ("new", "yac") and a removal ("name"). /// assert_eq!(jar.delta().count(), 3); /// ``` pub fn delta(&self) -> Delta { Delta { iter: self.delta_cookies.iter() } } /// Returns an iterator over all of the cookies present in this jar. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie}; /// /// let mut jar = CookieJar::new(); /// /// jar.add_original(("name", "value")); /// jar.add_original(("second", "two")); /// /// jar.add(("new", "third")); /// jar.add(("another", "fourth")); /// jar.add(("yac", "fifth")); /// /// jar.remove("name"); /// jar.remove("another"); /// /// // There are three cookies in the jar: "second", "new", and "yac". /// # assert_eq!(jar.iter().count(), 3); /// for cookie in jar.iter() { /// match cookie.name() { /// "second" => assert_eq!(cookie.value(), "two"), /// "new" => assert_eq!(cookie.value(), "third"), /// "yac" => assert_eq!(cookie.value(), "fifth"), /// _ => unreachable!("there are only three cookies in the jar") /// } /// } /// ``` pub fn iter(&self) -> Iter { Iter { delta_cookies: self.delta_cookies.iter() .chain(self.original_cookies.difference(&self.delta_cookies)), } } /// Returns a read-only `PrivateJar` with `self` as its parent jar using the /// key `key` to verify/decrypt cookies retrieved from the child jar. Any /// retrievals from the child jar will be made from the parent jar. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar, Key}; /// /// // Generate a secure key. /// let key = Key::generate(); /// /// // Add a private (signed + encrypted) cookie. /// let mut jar = CookieJar::new(); /// jar.private_mut(&key).add(("private", "text")); /// /// // The cookie's contents are encrypted. /// assert_ne!(jar.get("private").unwrap().value(), "text"); /// /// // They can be decrypted and verified through the child jar. /// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text"); /// /// // A tampered with cookie does not validate but still exists. /// let mut cookie = jar.get("private").unwrap().clone(); /// jar.add(("private", cookie.value().to_string() + "!")); /// assert!(jar.private(&key).get("private").is_none()); /// assert!(jar.get("private").is_some()); /// ``` #[cfg(feature = "private")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] pub fn private<'a>(&'a self, key: &Key) -> PrivateJar<&'a Self> { PrivateJar::new(self, key) } /// Returns a read/write `PrivateJar` with `self` as its parent jar using /// the key `key` to sign/encrypt and verify/decrypt cookies added/retrieved /// from the child jar. /// /// Any modifications to the child jar will be reflected on the parent jar, /// and any retrievals from the child jar will be made from the parent jar. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar, Key}; /// /// // Generate a secure key. /// let key = Key::generate(); /// /// // Add a private (signed + encrypted) cookie. /// let mut jar = CookieJar::new(); /// jar.private_mut(&key).add(("private", "text")); /// /// // Remove a cookie using the child jar. /// jar.private_mut(&key).remove("private"); /// ``` #[cfg(feature = "private")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] pub fn private_mut<'a>(&'a mut self, key: &Key) -> PrivateJar<&'a mut Self> { PrivateJar::new(self, key) } /// Returns a read-only `SignedJar` with `self` as its parent jar using the /// key `key` to verify cookies retrieved from the child jar. Any retrievals /// from the child jar will be made from the parent jar. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar, Key}; /// /// // Generate a secure key. /// let key = Key::generate(); /// /// // Add a signed cookie. /// let mut jar = CookieJar::new(); /// jar.signed_mut(&key).add(("signed", "text")); /// /// // The cookie's contents are signed but still in plaintext. /// assert_ne!(jar.get("signed").unwrap().value(), "text"); /// assert!(jar.get("signed").unwrap().value().contains("text")); /// /// // They can be verified through the child jar. /// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text"); /// /// // A tampered with cookie does not validate but still exists. /// let mut cookie = jar.get("signed").unwrap().clone(); /// jar.add(("signed", cookie.value().to_string() + "!")); /// assert!(jar.signed(&key).get("signed").is_none()); /// assert!(jar.get("signed").is_some()); /// ``` #[cfg(feature = "signed")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] pub fn signed<'a>(&'a self, key: &Key) -> SignedJar<&'a Self> { SignedJar::new(self, key) } /// Returns a read/write `SignedJar` with `self` as its parent jar using the /// key `key` to sign/verify cookies added/retrieved from the child jar. /// /// Any modifications to the child jar will be reflected on the parent jar, /// and any retrievals from the child jar will be made from the parent jar. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Key}; /// /// // Generate a secure key. /// let key = Key::generate(); /// /// // Add a signed cookie. /// let mut jar = CookieJar::new(); /// jar.signed_mut(&key).add(("signed", "text")); /// /// // Remove a cookie. /// jar.signed_mut(&key).remove("signed"); /// ``` #[cfg(feature = "signed")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] pub fn signed_mut<'a>(&'a mut self, key: &Key) -> SignedJar<&'a mut Self> { SignedJar::new(self, key) } /// Returns a read-only `PrefixedJar` with `self` as its parent jar that /// prefixes the name of cookies with `prefix`. Any retrievals from the /// child jar will be made from the parent jar. /// /// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning /// and definition are subject to change. /// /// # Example /// /// ```rust /// use cookie::CookieJar; /// use cookie::prefix::{Host, Secure}; /// /// // Add a `Host` prefixed cookie. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Host).add(("h0st", "value")); /// jar.prefixed_mut(Secure).add(("secur3", "value")); /// /// // The cookie's name is prefixed in the parent jar. /// assert!(matches!(jar.get("h0st"), None)); /// assert!(matches!(jar.get("__Host-h0st"), Some(_))); /// assert!(matches!(jar.get("secur3"), None)); /// assert!(matches!(jar.get("__Secure-secur3"), Some(_))); /// /// // The prefixed jar automatically removes the prefix. /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st"); /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value"); /// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().name(), "secur3"); /// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().value(), "value"); /// /// // Only the correct prefixed jar retrieves the cookie. /// assert!(matches!(jar.prefixed(Host).get("secur3"), None)); /// assert!(matches!(jar.prefixed(Secure).get("h0st"), None)); /// ``` #[inline(always)] pub fn prefixed<'a, P: Prefix>(&'a self, prefix: P) -> PrefixedJar { let _ = prefix; PrefixedJar::new(self) } /// Returns a read/write `PrefixedJar` with `self` as its parent jar that /// prefixes the name of cookies with `prefix` and makes the cookie conform /// to the prefix's requirements. This means that added cookies: /// /// 1. Have the [`Prefix::PREFIX`] prepended to their name. /// 2. Modify the cookie via [`Prefix::conform()`] so that it conforms to /// the prefix's requirements. /// /// Any modifications to the child jar will be reflected on the parent jar, /// and any retrievals from the child jar will be made from the parent jar. /// /// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning /// and definition are subject to change. /// /// # Example /// /// ```rust /// use cookie::CookieJar; /// use cookie::prefix::{Host, Secure}; /// /// // Add some prefixed cookies. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Host).add(("one", "1")); /// jar.prefixed_mut(Secure).add((2.to_string(), "2")); /// jar.prefixed_mut(Host).add((format!("{:0b}", 3), "0b11")); /// /// // Fetch cookies with either `prefixed()` or `prefixed_mut()`. /// assert_eq!(jar.prefixed(Host).get("one").unwrap().value(), "1"); /// assert_eq!(jar.prefixed(Secure).get("2").unwrap().value(), "2"); /// assert_eq!(jar.prefixed_mut(Host).get("11").unwrap().value(), "0b11"); /// /// // Remove cookies. /// jar.prefixed_mut(Host).remove("one"); /// assert!(jar.prefixed(Host).get("one").is_none()); /// ``` pub fn prefixed_mut<'a, P: Prefix>(&'a mut self, prefix: P) -> PrefixedJar { let _ = prefix; PrefixedJar::new(self) } } use std::collections::hash_set::Iter as HashSetIter; /// Iterator over the changes to a cookie jar. pub struct Delta<'a> { iter: HashSetIter<'a, DeltaCookie>, } impl<'a> Iterator for Delta<'a> { type Item = &'a Cookie<'static>; fn next(&mut self) -> Option<&'a Cookie<'static>> { self.iter.next().map(|c| &c.cookie) } } use std::collections::hash_set::Difference; use std::collections::hash_map::RandomState; use std::iter::Chain; /// Iterator over all of the cookies in a jar. pub struct Iter<'a> { delta_cookies: Chain, Difference<'a, DeltaCookie, RandomState>>, } impl<'a> Iterator for Iter<'a> { type Item = &'a Cookie<'static>; fn next(&mut self) -> Option<&'a Cookie<'static>> { for cookie in self.delta_cookies.by_ref() { if !cookie.removed { return Some(&*cookie); } } None } } #[cfg(test)] mod test { use super::CookieJar; use crate::Cookie; #[test] #[allow(deprecated)] fn simple() { let mut c = CookieJar::new(); c.add(("test", "")); c.add(("test2", "")); c.remove("test"); assert!(c.get("test").is_none()); assert!(c.get("test2").is_some()); c.add(("test3", "")); c.remove("test2"); c.remove("test3"); assert!(c.get("test").is_none()); assert!(c.get("test2").is_none()); assert!(c.get("test3").is_none()); } #[test] fn jar_is_send() { fn is_send(_: T) -> bool { true } assert!(is_send(CookieJar::new())) } #[test] #[cfg(all(feature = "signed", feature = "private"))] fn iter() { let key = crate::Key::generate(); let mut c = CookieJar::new(); c.add_original(Cookie::new("original", "original")); c.add(Cookie::new("test", "test")); c.add(Cookie::new("test2", "test2")); c.add(Cookie::new("test3", "test3")); assert_eq!(c.iter().count(), 4); c.signed_mut(&key).add(Cookie::new("signed", "signed")); c.private_mut(&key).add(Cookie::new("encrypted", "encrypted")); assert_eq!(c.iter().count(), 6); c.remove("test"); assert_eq!(c.iter().count(), 5); c.remove("signed"); c.remove("test2"); assert_eq!(c.iter().count(), 3); c.add(("test2", "test2")); assert_eq!(c.iter().count(), 4); c.remove("test2"); assert_eq!(c.iter().count(), 3); } #[test] fn delta() { use std::collections::HashMap; use time::Duration; let mut c = CookieJar::new(); c.add_original(Cookie::new("original", "original")); c.add_original(Cookie::new("original1", "original1")); c.add(Cookie::new("test", "test")); c.add(Cookie::new("test2", "test2")); c.add(Cookie::new("test3", "test3")); c.add(Cookie::new("test4", "test4")); c.remove("test"); c.remove("original"); assert_eq!(c.delta().count(), 4); let names: HashMap<_, _> = c.delta() .map(|c| (c.name(), c.max_age())) .collect(); assert!(names.get("test2").unwrap().is_none()); assert!(names.get("test3").unwrap().is_none()); assert!(names.get("test4").unwrap().is_none()); assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0))); } #[test] fn replace_original() { let mut jar = CookieJar::new(); jar.add_original(Cookie::new("original_a", "a")); jar.add_original(Cookie::new("original_b", "b")); assert_eq!(jar.get("original_a").unwrap().value(), "a"); jar.add(Cookie::new("original_a", "av2")); assert_eq!(jar.get("original_a").unwrap().value(), "av2"); } #[test] fn empty_delta() { let mut jar = CookieJar::new(); jar.add(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 1); jar.remove("name"); assert_eq!(jar.delta().count(), 0); jar.add_original(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 0); jar.remove("name"); assert_eq!(jar.delta().count(), 1); jar.add(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 1); jar.remove("name"); assert_eq!(jar.delta().count(), 1); } #[test] fn add_remove_add() { let mut jar = CookieJar::new(); jar.add_original(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 0); jar.remove("name"); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); assert_eq!(jar.delta().count(), 1); // The cookie's been deleted. Another original doesn't change that. jar.add_original(Cookie::new("name", "val")); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); assert_eq!(jar.delta().count(), 1); jar.remove("name"); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); assert_eq!(jar.delta().count(), 1); jar.add(Cookie::new("name", "val")); assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); assert_eq!(jar.delta().count(), 1); jar.remove("name"); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); assert_eq!(jar.delta().count(), 1); } #[test] fn replace_remove() { let mut jar = CookieJar::new(); jar.add_original(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 0); jar.add(Cookie::new("name", "val")); assert_eq!(jar.delta().count(), 1); assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); jar.remove("name"); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); } #[test] fn remove_with_path() { let mut jar = CookieJar::new(); jar.add_original(("name", "val")); assert_eq!(jar.iter().count(), 1); assert_eq!(jar.delta().count(), 0); assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1); jar.remove(Cookie::build("name").path("/")); assert_eq!(jar.iter().count(), 0); assert_eq!(jar.delta().count(), 1); assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1); } } cookie-0.18.0/src/lib.rs000064400000000000000000001611531046102023000130750ustar 00000000000000//! HTTP cookie parsing and cookie jar management. //! //! This crates provides the [`Cookie`] type, representing an HTTP cookie, and //! the [`CookieJar`] type, which manages a collection of cookies for session //! management, recording changes as they are made, and optional automatic //! cookie encryption and signing. //! //! # Usage //! //! Add the following to the `[dependencies]` section of your `Cargo.toml`: //! //! ```toml //! cookie = "0.18" //! ``` //! //! # Features //! //! This crate exposes several features, all of which are disabled by default: //! //! * **`percent-encode`** //! //! Enables _percent encoding and decoding_ of names and values in cookies. //! //! When this feature is enabled, the [`Cookie::encoded()`] and //! [`Cookie::parse_encoded()`] methods are available. The `encoded` method //! returns a wrapper around a `Cookie` whose `Display` implementation //! percent-encodes the name and value of the cookie. The `parse_encoded` //! method percent-decodes the name and value of a `Cookie` during parsing. //! //! * **`signed`** //! //! Enables _signed_ cookies via [`CookieJar::signed()`]. //! //! When this feature is enabled, the [`CookieJar::signed()`] method, //! [`SignedJar`] type, and [`Key`] type are available. The jar acts as "child //! jar"; operations on the jar automatically sign and verify cookies as they //! are added and retrieved from the parent jar. //! //! * **`private`** //! //! Enables _private_ (authenticated, encrypted) cookies via //! [`CookieJar::private()`]. //! //! When this feature is enabled, the [`CookieJar::private()`] method, //! [`PrivateJar`] type, and [`Key`] type are available. The jar acts as "child //! jar"; operations on the jar automatically encrypt and decrypt/authenticate //! cookies as they are added and retrieved from the parent jar. //! //! * **`key-expansion`** //! //! Enables _key expansion_ or _key derivation_ via [`Key::derive_from()`]. //! //! When this feature is enabled, and either `signed` or `private` are _also_ //! enabled, the [`Key::derive_from()`] method is available. The method can be //! used to derive a `Key` structure appropriate for use with signed and //! private jars from cryptographically valid key material that is shorter in //! length than the full key. //! //! * **`secure`** //! //! A meta-feature that simultaneously enables `signed`, `private`, and //! `key-expansion`. //! //! You can enable features via `Cargo.toml`: //! //! ```toml //! [dependencies.cookie] //! features = ["secure", "percent-encode"] //! ``` #![cfg_attr(all(nightly, doc), feature(doc_cfg))] #![deny(missing_docs)] pub use time; mod builder; mod parse; mod jar; mod delta; mod same_site; mod expiration; /// Implementation of [HTTP RFC6265 draft] cookie prefixes. /// /// [HTTP RFC6265 draft]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes pub mod prefix; #[cfg(any(feature = "private", feature = "signed"))] #[macro_use] mod secure; #[cfg(any(feature = "private", feature = "signed"))] pub use secure::*; use std::borrow::Cow; use std::fmt; use std::str::FromStr; #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; use crate::parse::parse_cookie; pub use crate::parse::ParseError; pub use crate::builder::CookieBuilder; pub use crate::jar::{CookieJar, Delta, Iter}; pub use crate::same_site::*; pub use crate::expiration::*; #[derive(Debug, Clone)] enum CookieStr<'c> { /// An string derived from indexes (start, end). Indexed(usize, usize), /// A string derived from a concrete string. Concrete(Cow<'c, str>), } impl<'c> CookieStr<'c> { /// Creates an indexed `CookieStr` that holds the start and end indices of /// `needle` inside of `haystack`, if `needle` is a substring of `haystack`. /// Otherwise returns `None`. /// /// The `needle` can later be retrieved via `to_str()`. fn indexed(needle: &str, haystack: &str) -> Option> { let haystack_start = haystack.as_ptr() as usize; let needle_start = needle.as_ptr() as usize; if needle_start < haystack_start { return None; } if (needle_start + needle.len()) > (haystack_start + haystack.len()) { return None; } let start = needle_start - haystack_start; let end = start + needle.len(); Some(CookieStr::Indexed(start, end)) } /// Retrieves the string `self` corresponds to. If `self` is derived from /// indices, the corresponding subslice of `string` is returned. Otherwise, /// the concrete string is returned. /// /// # Panics /// /// Panics if `self` is an indexed string and `string` is None. fn to_str<'s>(&'s self, string: Option<&'s Cow>) -> &'s str { match *self { CookieStr::Indexed(i, j) => { let s = string.expect("`Some` base string must exist when \ converting indexed str to str! (This is a module invariant.)"); &s[i..j] }, CookieStr::Concrete(ref cstr) => &*cstr, } } #[allow(clippy::ptr_arg)] fn to_raw_str<'s, 'b: 's>(&'s self, string: &'s Cow<'b, str>) -> Option<&'b str> { match *self { CookieStr::Indexed(i, j) => { match *string { Cow::Borrowed(s) => Some(&s[i..j]), Cow::Owned(_) => None, } }, CookieStr::Concrete(_) => None, } } fn into_owned(self) -> CookieStr<'static> { use crate::CookieStr::*; match self { Indexed(a, b) => Indexed(a, b), Concrete(Cow::Owned(c)) => Concrete(Cow::Owned(c)), Concrete(Cow::Borrowed(c)) => Concrete(Cow::Owned(c.into())), } } } /// Representation of an HTTP cookie. /// /// ## Constructing a `Cookie` /// /// To construct a cookie with only a name/value, use [`Cookie::new()`]: /// /// ```rust /// use cookie::Cookie; /// /// let cookie = Cookie::new("name", "value"); /// assert_eq!(cookie.to_string(), "name=value"); /// ``` /// /// ## Building a `Cookie` /// /// To construct more elaborate cookies, use [`Cookie::build()`] and /// [`CookieBuilder`] methods. `Cookie::build()` accepts any type that /// implements `T: Into`. See [`Cookie::build()`] for details. /// /// ```rust /// use cookie::Cookie; /// /// let cookie = Cookie::build(("name", "value")) /// .domain("www.rust-lang.org") /// .path("/") /// .secure(true) /// .http_only(true); /// /// # let mut jar = cookie::CookieJar::new(); /// jar.add(cookie); /// jar.remove(Cookie::build("name").path("/")); /// ``` #[derive(Debug, Clone)] pub struct Cookie<'c> { /// Storage for the cookie string. Only used if this structure was derived /// from a string that was subsequently parsed. cookie_string: Option>, /// The cookie's name. name: CookieStr<'c>, /// The cookie's value. value: CookieStr<'c>, /// The cookie's expiration, if any. expires: Option, /// The cookie's maximum age, if any. max_age: Option, /// The cookie's domain, if any. domain: Option>, /// The cookie's path domain, if any. path: Option>, /// Whether this cookie was marked Secure. secure: Option, /// Whether this cookie was marked HttpOnly. http_only: Option, /// The draft `SameSite` attribute. same_site: Option, } impl<'c> Cookie<'c> { /// Creates a new `Cookie` with the given name and value. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let cookie = Cookie::new("name", "value"); /// assert_eq!(cookie.name_value(), ("name", "value")); /// /// // This is equivalent to `from` with a `(name, value)` tuple: /// let cookie = Cookie::from(("name", "value")); /// assert_eq!(cookie.name_value(), ("name", "value")); /// ``` pub fn new(name: N, value: V) -> Self where N: Into>, V: Into> { Cookie { cookie_string: None, name: CookieStr::Concrete(name.into()), value: CookieStr::Concrete(value.into()), expires: None, max_age: None, domain: None, path: None, secure: None, http_only: None, same_site: None, } } /// Creates a new `Cookie` with the given name and an empty value. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let cookie = Cookie::named("name"); /// assert_eq!(cookie.name(), "name"); /// assert!(cookie.value().is_empty()); /// /// // This is equivalent to `from` with `"name`: /// let cookie = Cookie::from("name"); /// assert_eq!(cookie.name(), "name"); /// assert!(cookie.value().is_empty()); /// ``` #[deprecated(since = "0.18.0", note = "use `Cookie::build(name)` or `Cookie::from(name)`")] pub fn named(name: N) -> Cookie<'c> where N: Into> { Cookie::new(name, "") } /// Creates a new [`CookieBuilder`] starting from a `base` cookie. /// /// Any type that implements `T: Into` can be used as a `base`: /// /// | `Into` Type | Example | Equivalent To | /// |----------------------------------|------------------------|----------------------------| /// | `(K, V)`, `K, V: Into>` | `("name", "value")` | `Cookie::new(name, value)` | /// | `&str`, `String`, `Cow` | `"name"` | `Cookie::new(name, "")` | /// | [`CookieBuilder`] | `Cookie::build("foo")` | [`CookieBuilder::build()`] | /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// // Use `(K, V)` as the base, setting a name and value. /// let b1 = Cookie::build(("name", "value")).path("/"); /// assert_eq!(b1.inner().name_value(), ("name", "value")); /// assert_eq!(b1.inner().path(), Some("/")); /// /// // Use `&str` as the base, setting a name and empty value. /// let b2 = Cookie::build(("name")); /// assert_eq!(b2.inner().name_value(), ("name", "")); /// /// // Use `CookieBuilder` as the base, inheriting all properties. /// let b3 = Cookie::build(b1); /// assert_eq!(b3.inner().name_value(), ("name", "value")); /// assert_eq!(b3.inner().path(), Some("/")); /// ``` pub fn build>>(base: C) -> CookieBuilder<'c> { CookieBuilder::from(base.into()) } /// Parses a `Cookie` from the given HTTP cookie header value string. Does /// not perform any percent-decoding. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("foo=bar%20baz; HttpOnly").unwrap(); /// assert_eq!(c.name_value(), ("foo", "bar%20baz")); /// assert_eq!(c.http_only(), Some(true)); /// assert_eq!(c.secure(), None); /// ``` pub fn parse(s: S) -> Result, ParseError> where S: Into> { parse_cookie(s.into(), false) } /// Parses a `Cookie` from the given HTTP cookie header value string where /// the name and value fields are percent-encoded. Percent-decodes the /// name/value fields. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse_encoded("foo=bar%20baz; HttpOnly").unwrap(); /// assert_eq!(c.name_value(), ("foo", "bar baz")); /// assert_eq!(c.http_only(), Some(true)); /// assert_eq!(c.secure(), None); /// ``` #[cfg(feature = "percent-encode")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] pub fn parse_encoded(s: S) -> Result, ParseError> where S: Into> { parse_cookie(s.into(), true) } /// Parses the HTTP `Cookie` header, a series of cookie names and value /// separated by `;`, returning an iterator over the parse results. Each /// item returned by the iterator is a `Result` of /// parsing one name/value pair. Empty cookie values (i.e, in `a=1;;b=2`) /// and any excess surrounding whitespace are ignored. /// /// Unlike [`Cookie::split_parse_encoded()`], this method _does **not**_ /// percent-decode keys and values. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let string = "name=value; other=key%20value"; /// # let values: Vec<_> = Cookie::split_parse(string).collect(); /// # assert_eq!(values.len(), 2); /// # assert_eq!(values[0].as_ref().unwrap().name(), "name"); /// # assert_eq!(values[1].as_ref().unwrap().name(), "other"); /// for cookie in Cookie::split_parse(string) { /// let cookie = cookie.unwrap(); /// match cookie.name() { /// "name" => assert_eq!(cookie.value(), "value"), /// "other" => assert_eq!(cookie.value(), "key%20value"), /// _ => unreachable!() /// } /// } /// ``` #[inline(always)] pub fn split_parse(string: S) -> SplitCookies<'c> where S: Into> { SplitCookies { string: string.into(), last: 0, decode: false, } } /// Parses the HTTP `Cookie` header, a series of cookie names and value /// separated by `;`, returning an iterator over the parse results. Each /// item returned by the iterator is a `Result` of /// parsing one name/value pair. Empty cookie values (i.e, in `a=1;;b=2`) /// and any excess surrounding whitespace are ignored. /// /// Unlike [`Cookie::split_parse()`], this method _does_ percent-decode keys /// and values. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let string = "name=value; other=key%20value"; /// # let v: Vec<_> = Cookie::split_parse_encoded(string).collect(); /// # assert_eq!(v.len(), 2); /// # assert_eq!(v[0].as_ref().unwrap().name_value(), ("name", "value")); /// # assert_eq!(v[1].as_ref().unwrap().name_value(), ("other", "key value")); /// for cookie in Cookie::split_parse_encoded(string) { /// let cookie = cookie.unwrap(); /// match cookie.name() { /// "name" => assert_eq!(cookie.value(), "value"), /// "other" => assert_eq!(cookie.value(), "key value"), /// _ => unreachable!() /// } /// } /// ``` #[cfg(feature = "percent-encode")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] #[inline(always)] pub fn split_parse_encoded(string: S) -> SplitCookies<'c> where S: Into> { SplitCookies { string: string.into(), last: 0, decode: true, } } /// Converts `self` into a `Cookie` with a static lifetime with as few /// allocations as possible. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::new("a", "b"); /// let owned_cookie = c.into_owned(); /// assert_eq!(owned_cookie.name_value(), ("a", "b")); /// ``` pub fn into_owned(self) -> Cookie<'static> { Cookie { cookie_string: self.cookie_string.map(|s| s.into_owned().into()), name: self.name.into_owned(), value: self.value.into_owned(), expires: self.expires, max_age: self.max_age, domain: self.domain.map(|s| s.into_owned()), path: self.path.map(|s| s.into_owned()), secure: self.secure, http_only: self.http_only, same_site: self.same_site, } } /// Returns the name of `self`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::new("name", "value"); /// assert_eq!(c.name(), "name"); /// ``` #[inline] pub fn name(&self) -> &str { self.name.to_str(self.cookie_string.as_ref()) } /// Returns the value of `self`. /// /// Does not strip surrounding quotes. See [`Cookie::value_trimmed()`] for a /// version that does. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::new("name", "value"); /// assert_eq!(c.value(), "value"); /// /// let c = Cookie::new("name", "\"value\""); /// assert_eq!(c.value(), "\"value\""); /// ``` #[inline] pub fn value(&self) -> &str { self.value.to_str(self.cookie_string.as_ref()) } /// Returns the value of `self` with surrounding double-quotes trimmed. /// /// This is _not_ the value of the cookie (_that_ is [`Cookie::value()`]). /// Instead, this is the value with a surrounding pair of double-quotes, if /// any, trimmed away. Quotes are only trimmed when they form a pair and /// never otherwise. The trimmed value is never used for other operations, /// such as equality checking, on `self`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// let c0 = Cookie::new("name", "value"); /// assert_eq!(c0.value_trimmed(), "value"); /// /// let c = Cookie::new("name", "\"value\""); /// assert_eq!(c.value_trimmed(), "value"); /// assert!(c != c0); /// /// let c = Cookie::new("name", "\"value"); /// assert_eq!(c.value(), "\"value"); /// assert_eq!(c.value_trimmed(), "\"value"); /// assert!(c != c0); /// /// let c = Cookie::new("name", "\"value\"\""); /// assert_eq!(c.value(), "\"value\"\""); /// assert_eq!(c.value_trimmed(), "value\""); /// assert!(c != c0); /// ``` #[inline] pub fn value_trimmed(&self) -> &str { #[inline(always)] fn trim_quotes(s: &str) -> &str { if s.len() < 2 { return s; } let bytes = s.as_bytes(); match (bytes.first(), bytes.last()) { (Some(b'"'), Some(b'"')) => &s[1..(s.len() - 1)], _ => s } } trim_quotes(self.value()) } /// Returns the name and value of `self` as a tuple of `(name, value)`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::new("name", "value"); /// assert_eq!(c.name_value(), ("name", "value")); /// ``` #[inline] pub fn name_value(&self) -> (&str, &str) { (self.name(), self.value()) } /// Returns the name and [trimmed value](Cookie::value_trimmed()) of `self` /// as a tuple of `(name, trimmed_value)`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::new("name", "\"value\""); /// assert_eq!(c.name_value_trimmed(), ("name", "value")); /// ``` #[inline] pub fn name_value_trimmed(&self) -> (&str, &str) { (self.name(), self.value_trimmed()) } /// Returns whether this cookie was marked `HttpOnly` or not. Returns /// `Some(true)` when the cookie was explicitly set (manually or parsed) as /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`, /// and `None` otherwise. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value; httponly").unwrap(); /// assert_eq!(c.http_only(), Some(true)); /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.http_only(), None); /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.http_only(), None); /// /// // An explicitly set "false" value. /// c.set_http_only(false); /// assert_eq!(c.http_only(), Some(false)); /// /// // An explicitly set "true" value. /// c.set_http_only(true); /// assert_eq!(c.http_only(), Some(true)); /// ``` #[inline] pub fn http_only(&self) -> Option { self.http_only } /// Returns whether this cookie was marked `Secure` or not. Returns /// `Some(true)` when the cookie was explicitly set (manually or parsed) as /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and /// `None` otherwise. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value; Secure").unwrap(); /// assert_eq!(c.secure(), Some(true)); /// /// let mut c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.secure(), None); /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.secure(), None); /// /// // An explicitly set "false" value. /// c.set_secure(false); /// assert_eq!(c.secure(), Some(false)); /// /// // An explicitly set "true" value. /// c.set_secure(true); /// assert_eq!(c.secure(), Some(true)); /// ``` #[inline] pub fn secure(&self) -> Option { self.secure } /// Returns the `SameSite` attribute of this cookie if one was specified. /// /// # Example /// /// ``` /// use cookie::{Cookie, SameSite}; /// /// let c = Cookie::parse("name=value; SameSite=Lax").unwrap(); /// assert_eq!(c.same_site(), Some(SameSite::Lax)); /// ``` #[inline] pub fn same_site(&self) -> Option { self.same_site } /// Returns the specified max-age of the cookie if one was specified. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.max_age(), None); /// /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); /// ``` #[inline] pub fn max_age(&self) -> Option { self.max_age } /// Returns the `Path` of the cookie if one was specified. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.path(), None); /// /// let c = Cookie::parse("name=value; Path=/").unwrap(); /// assert_eq!(c.path(), Some("/")); /// /// let c = Cookie::parse("name=value; path=/sub").unwrap(); /// assert_eq!(c.path(), Some("/sub")); /// ``` #[inline] pub fn path(&self) -> Option<&str> { match self.path { Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), None => None, } } /// Returns the `Domain` of the cookie if one was specified. /// /// This does not consider whether the `Domain` is valid; validation is left /// to higher-level libraries, as needed. However, if the `Domain` starts /// with a leading `.`, the leading `.` is stripped. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.domain(), None); /// /// let c = Cookie::parse("name=value; Domain=crates.io").unwrap(); /// assert_eq!(c.domain(), Some("crates.io")); /// /// let c = Cookie::parse("name=value; Domain=.crates.io").unwrap(); /// assert_eq!(c.domain(), Some("crates.io")); /// /// // Note that `..crates.io` is not a valid domain. /// let c = Cookie::parse("name=value; Domain=..crates.io").unwrap(); /// assert_eq!(c.domain(), Some(".crates.io")); /// ``` #[inline] pub fn domain(&self) -> Option<&str> { match self.domain { Some(ref c) => { let domain = c.to_str(self.cookie_string.as_ref()); domain.strip_prefix(".").or(Some(domain)) }, None => None, } } /// Returns the [`Expiration`] of the cookie if one was specified. /// /// # Example /// /// ``` /// use cookie::{Cookie, Expiration}; /// /// let c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.expires(), None); /// /// // Here, `cookie.expires_datetime()` returns `None`. /// let c = Cookie::build(("name", "value")).expires(None).build(); /// assert_eq!(c.expires(), Some(Expiration::Session)); /// /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; /// let cookie_str = format!("name=value; Expires={}", expire_time); /// let c = Cookie::parse(cookie_str).unwrap(); /// assert_eq!(c.expires().and_then(|e| e.datetime()).map(|t| t.year()), Some(2017)); /// ``` #[inline] pub fn expires(&self) -> Option { self.expires } /// Returns the expiration date-time of the cookie if one was specified. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let c = Cookie::parse("name=value").unwrap(); /// assert_eq!(c.expires_datetime(), None); /// /// // Here, `cookie.expires()` returns `Some`. /// let c = Cookie::build(("name", "value")).expires(None).build(); /// assert_eq!(c.expires_datetime(), None); /// /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; /// let cookie_str = format!("name=value; Expires={}", expire_time); /// let c = Cookie::parse(cookie_str).unwrap(); /// assert_eq!(c.expires_datetime().map(|t| t.year()), Some(2017)); /// ``` #[inline] pub fn expires_datetime(&self) -> Option { self.expires.and_then(|e| e.datetime()) } /// Sets the name of `self` to `name`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.name(), "name"); /// /// c.set_name("foo"); /// assert_eq!(c.name(), "foo"); /// ``` pub fn set_name>>(&mut self, name: N) { self.name = CookieStr::Concrete(name.into()) } /// Sets the value of `self` to `value`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.value(), "value"); /// /// c.set_value("bar"); /// assert_eq!(c.value(), "bar"); /// ``` pub fn set_value>>(&mut self, value: V) { self.value = CookieStr::Concrete(value.into()) } /// Sets the value of `http_only` in `self` to `value`. If `value` is /// `None`, the field is unset. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.http_only(), None); /// /// c.set_http_only(true); /// assert_eq!(c.http_only(), Some(true)); /// /// c.set_http_only(false); /// assert_eq!(c.http_only(), Some(false)); /// /// c.set_http_only(None); /// assert_eq!(c.http_only(), None); /// ``` #[inline] pub fn set_http_only>>(&mut self, value: T) { self.http_only = value.into(); } /// Sets the value of `secure` in `self` to `value`. If `value` is `None`, /// the field is unset. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.secure(), None); /// /// c.set_secure(true); /// assert_eq!(c.secure(), Some(true)); /// /// c.set_secure(false); /// assert_eq!(c.secure(), Some(false)); /// /// c.set_secure(None); /// assert_eq!(c.secure(), None); /// ``` #[inline] pub fn set_secure>>(&mut self, value: T) { self.secure = value.into(); } /// Sets the value of `same_site` in `self` to `value`. If `value` is /// `None`, the field is unset. If `value` is `SameSite::None`, the "Secure" /// flag will be set when the cookie is written out unless `secure` is /// explicitly set to `false` via [`Cookie::set_secure()`] or the equivalent /// builder method. /// /// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 /// /// # Example /// /// ``` /// use cookie::{Cookie, SameSite}; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.same_site(), None); /// /// c.set_same_site(SameSite::None); /// assert_eq!(c.same_site(), Some(SameSite::None)); /// assert_eq!(c.to_string(), "name=value; SameSite=None; Secure"); /// /// c.set_secure(false); /// assert_eq!(c.to_string(), "name=value; SameSite=None"); /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.same_site(), None); /// /// c.set_same_site(SameSite::Strict); /// assert_eq!(c.same_site(), Some(SameSite::Strict)); /// assert_eq!(c.to_string(), "name=value; SameSite=Strict"); /// /// c.set_same_site(None); /// assert_eq!(c.same_site(), None); /// assert_eq!(c.to_string(), "name=value"); /// ``` #[inline] pub fn set_same_site>>(&mut self, value: T) { self.same_site = value.into(); } /// Sets the value of `max_age` in `self` to `value`. If `value` is `None`, /// the field is unset. /// /// # Example /// /// ```rust /// # extern crate cookie; /// use cookie::Cookie; /// use cookie::time::Duration; /// /// # fn main() { /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.max_age(), None); /// /// c.set_max_age(Duration::hours(10)); /// assert_eq!(c.max_age(), Some(Duration::hours(10))); /// /// c.set_max_age(None); /// assert!(c.max_age().is_none()); /// # } /// ``` #[inline] pub fn set_max_age>>(&mut self, value: D) { self.max_age = value.into(); } /// Sets the `path` of `self` to `path`. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.path(), None); /// /// c.set_path("/"); /// assert_eq!(c.path(), Some("/")); /// ``` pub fn set_path>>(&mut self, path: P) { self.path = Some(CookieStr::Concrete(path.into())); } /// Unsets the `path` of `self`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.path(), None); /// /// c.set_path("/"); /// assert_eq!(c.path(), Some("/")); /// /// c.unset_path(); /// assert_eq!(c.path(), None); /// ``` pub fn unset_path(&mut self) { self.path = None; } /// Sets the `domain` of `self` to `domain`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.domain(), None); /// /// c.set_domain("rust-lang.org"); /// assert_eq!(c.domain(), Some("rust-lang.org")); /// ``` pub fn set_domain>>(&mut self, domain: D) { self.domain = Some(CookieStr::Concrete(domain.into())); } /// Unsets the `domain` of `self`. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.domain(), None); /// /// c.set_domain("rust-lang.org"); /// assert_eq!(c.domain(), Some("rust-lang.org")); /// /// c.unset_domain(); /// assert_eq!(c.domain(), None); /// ``` pub fn unset_domain(&mut self) { self.domain = None; } /// Sets the expires field of `self` to `time`. If `time` is `None`, an /// expiration of [`Session`](Expiration::Session) is set. /// /// # Example /// /// ``` /// # extern crate cookie; /// use cookie::{Cookie, Expiration}; /// use cookie::time::{Duration, OffsetDateTime}; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.expires(), None); /// /// let mut now = OffsetDateTime::now_utc(); /// now += Duration::weeks(52); /// /// c.set_expires(now); /// assert!(c.expires().is_some()); /// /// c.set_expires(None); /// assert_eq!(c.expires(), Some(Expiration::Session)); /// ``` pub fn set_expires>(&mut self, time: T) { static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); // RFC 6265 requires dates not to exceed 9999 years. self.expires = Some(time.into() .map(|time| std::cmp::min(time, MAX_DATETIME))); } /// Unsets the `expires` of `self`. /// /// # Example /// /// ``` /// use cookie::{Cookie, Expiration}; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.expires(), None); /// /// c.set_expires(None); /// assert_eq!(c.expires(), Some(Expiration::Session)); /// /// c.unset_expires(); /// assert_eq!(c.expires(), None); /// ``` pub fn unset_expires(&mut self) { self.expires = None; } /// Makes `self` a "permanent" cookie by extending its expiration and max /// age 20 years into the future. /// /// # Example /// /// ```rust /// # extern crate cookie; /// use cookie::Cookie; /// use cookie::time::Duration; /// /// # fn main() { /// let mut c = Cookie::new("foo", "bar"); /// assert!(c.expires().is_none()); /// assert!(c.max_age().is_none()); /// /// c.make_permanent(); /// assert!(c.expires().is_some()); /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); /// # } /// ``` pub fn make_permanent(&mut self) { let twenty_years = Duration::days(365 * 20); self.set_max_age(twenty_years); self.set_expires(OffsetDateTime::now_utc() + twenty_years); } /// Make `self` a "removal" cookie by clearing its value, setting a max-age /// of `0`, and setting an expiration date far in the past. /// /// # Example /// /// ```rust /// # extern crate cookie; /// use cookie::Cookie; /// use cookie::time::Duration; /// /// # fn main() { /// let mut c = Cookie::new("foo", "bar"); /// c.make_permanent(); /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); /// assert_eq!(c.value(), "bar"); /// /// c.make_removal(); /// assert_eq!(c.value(), ""); /// assert_eq!(c.max_age(), Some(Duration::ZERO)); /// # } /// ``` pub fn make_removal(&mut self) { self.set_value(""); self.set_max_age(Duration::seconds(0)); self.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); } fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(true) = self.http_only() { write!(f, "; HttpOnly")?; } if let Some(same_site) = self.same_site() { write!(f, "; SameSite={}", same_site)?; if same_site.is_none() && self.secure().is_none() { write!(f, "; Secure")?; } } if let Some(true) = self.secure() { write!(f, "; Secure")?; } if let Some(path) = self.path() { write!(f, "; Path={}", path)?; } if let Some(domain) = self.domain() { write!(f, "; Domain={}", domain)?; } if let Some(max_age) = self.max_age() { write!(f, "; Max-Age={}", max_age.whole_seconds())?; } if let Some(time) = self.expires_datetime() { let time = time.to_offset(UtcOffset::UTC); write!(f, "; Expires={}", time.format(&crate::parse::FMT1).map_err(|_| fmt::Error)?)?; } Ok(()) } /// Returns the name of `self` as a string slice of the raw string `self` /// was originally parsed from. If `self` was not originally parsed from a /// raw string, returns `None`. /// /// This method differs from [`Cookie::name()`] in that it returns a string /// with the same lifetime as the originally parsed string. This lifetime /// may outlive `self`. If a longer lifetime is not required, or you're /// unsure if you need a longer lifetime, use [`Cookie::name()`]. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let cookie_string = format!("{}={}", "foo", "bar"); /// /// // `c` will be dropped at the end of the scope, but `name` will live on /// let name = { /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); /// c.name_raw() /// }; /// /// assert_eq!(name, Some("foo")); /// ``` #[inline] pub fn name_raw(&self) -> Option<&'c str> { self.cookie_string.as_ref() .and_then(|s| self.name.to_raw_str(s)) } /// Returns the value of `self` as a string slice of the raw string `self` /// was originally parsed from. If `self` was not originally parsed from a /// raw string, returns `None`. /// /// This method differs from [`Cookie::value()`] in that it returns a /// string with the same lifetime as the originally parsed string. This /// lifetime may outlive `self`. If a longer lifetime is not required, or /// you're unsure if you need a longer lifetime, use [`Cookie::value()`]. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let cookie_string = format!("{}={}", "foo", "bar"); /// /// // `c` will be dropped at the end of the scope, but `value` will live on /// let value = { /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); /// c.value_raw() /// }; /// /// assert_eq!(value, Some("bar")); /// ``` #[inline] pub fn value_raw(&self) -> Option<&'c str> { self.cookie_string.as_ref() .and_then(|s| self.value.to_raw_str(s)) } /// Returns the `Path` of `self` as a string slice of the raw string `self` /// was originally parsed from. If `self` was not originally parsed from a /// raw string, or if `self` doesn't contain a `Path`, or if the `Path` has /// changed since parsing, returns `None`. /// /// This method differs from [`Cookie::path()`] in that it returns a /// string with the same lifetime as the originally parsed string. This /// lifetime may outlive `self`. If a longer lifetime is not required, or /// you're unsure if you need a longer lifetime, use [`Cookie::path()`]. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let cookie_string = format!("{}={}; Path=/", "foo", "bar"); /// /// // `c` will be dropped at the end of the scope, but `path` will live on /// let path = { /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); /// c.path_raw() /// }; /// /// assert_eq!(path, Some("/")); /// ``` #[inline] pub fn path_raw(&self) -> Option<&'c str> { match (self.path.as_ref(), self.cookie_string.as_ref()) { (Some(path), Some(string)) => path.to_raw_str(string), _ => None, } } /// Returns the `Domain` of `self` as a string slice of the raw string /// `self` was originally parsed from. If `self` was not originally parsed /// from a raw string, or if `self` doesn't contain a `Domain`, or if the /// `Domain` has changed since parsing, returns `None`. /// /// Like [`Cookie::domain()`], this does not consider whether `Domain` is /// valid; validation is left to higher-level libraries, as needed. However, /// if `Domain` starts with a leading `.`, the leading `.` is stripped. /// /// This method differs from [`Cookie::domain()`] in that it returns a /// string with the same lifetime as the originally parsed string. This /// lifetime may outlive `self` struct. If a longer lifetime is not /// required, or you're unsure if you need a longer lifetime, use /// [`Cookie::domain()`]. /// /// # Example /// /// ``` /// use cookie::Cookie; /// /// let cookie_string = format!("{}={}; Domain=.crates.io", "foo", "bar"); /// /// //`c` will be dropped at the end of the scope, but `domain` will live on /// let domain = { /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); /// c.domain_raw() /// }; /// /// assert_eq!(domain, Some("crates.io")); /// ``` #[inline] pub fn domain_raw(&self) -> Option<&'c str> { match (self.domain.as_ref(), self.cookie_string.as_ref()) { (Some(domain), Some(string)) => match domain.to_raw_str(string) { Some(s) => s.strip_prefix(".").or(Some(s)), None => None, } _ => None, } } /// Wraps `self` in an encoded [`Display`]: a cost-free wrapper around /// `Cookie` whose [`fmt::Display`] implementation percent-encodes the name /// and value of the wrapped `Cookie`. /// /// The returned structure can be chained with [`Display::stripped()`] to /// display only the name and value. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let mut c = Cookie::build(("my name", "this; value?")).secure(true).build(); /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F; Secure"); /// assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%3F"); /// ``` #[cfg(feature = "percent-encode")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] #[inline(always)] pub fn encoded<'a>(&'a self) -> Display<'a, 'c> { Display::new_encoded(self) } /// Wraps `self` in a stripped `Display`]: a cost-free wrapper around /// `Cookie` whose [`fmt::Display`] implementation prints only the `name` /// and `value` of the wrapped `Cookie`. /// /// The returned structure can be chained with [`Display::encoded()`] to /// encode the name and value. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let mut c = Cookie::build(("key?", "value")).secure(true).path("/").build(); /// assert_eq!(&c.stripped().to_string(), "key?=value"); #[cfg_attr(feature = "percent-encode", doc = r##" // Note: `encoded()` is only available when `percent-encode` is enabled. assert_eq!(&c.stripped().encoded().to_string(), "key%3F=value"); #"##)] /// ``` #[inline(always)] pub fn stripped<'a>(&'a self) -> Display<'a, 'c> { Display::new_stripped(self) } } /// An iterator over cookie parse `Result`s: `Result`. /// /// Returned by [`Cookie::split_parse()`] and [`Cookie::split_parse_encoded()`]. pub struct SplitCookies<'c> { // The source string, which we split and parse. string: Cow<'c, str>, // The index where we last split off. last: usize, // Whether we should percent-decode when parsing. decode: bool, } impl<'c> Iterator for SplitCookies<'c> { type Item = Result, ParseError>; fn next(&mut self) -> Option { while self.last < self.string.len() { let i = self.last; let j = self.string[i..] .find(';') .map(|k| i + k) .unwrap_or(self.string.len()); self.last = j + 1; if self.string[i..j].chars().all(|c| c.is_whitespace()) { continue; } return Some(match self.string { Cow::Borrowed(s) => parse_cookie(s[i..j].trim(), self.decode), Cow::Owned(ref s) => parse_cookie(s[i..j].trim().to_owned(), self.decode), }) } None } } #[cfg(feature = "percent-encode")] mod encoding { use percent_encoding::{AsciiSet, CONTROLS}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const FRAGMENT: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') .add(b'<') .add(b'>') .add(b'`'); /// https://url.spec.whatwg.org/#path-percent-encode-set const PATH: &AsciiSet = &FRAGMENT .add(b'#') .add(b'?') .add(b'{') .add(b'}'); /// https://url.spec.whatwg.org/#userinfo-percent-encode-set const USERINFO: &AsciiSet = &PATH .add(b'/') .add(b':') .add(b';') .add(b'=') .add(b'@') .add(b'[') .add(b'\\') .add(b']') .add(b'^') .add(b'|') .add(b'%'); /// https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + '(', ')' const COOKIE: &AsciiSet = &USERINFO .add(b'(') .add(b')') .add(b','); /// Percent-encode a cookie name or value with the proper encoding set. pub fn encode(string: &str) -> impl std::fmt::Display + '_ { percent_encoding::percent_encode(string.as_bytes(), COOKIE) } } /// Wrapper around `Cookie` whose `Display` implementation either /// percent-encodes the cookie's name and value, skips displaying the cookie's /// parameters (only displaying it's name and value), or both. /// /// A value of this type can be obtained via [`Cookie::encoded()`] and /// [`Cookie::stripped()`], or an arbitrary chaining of the two methods. This /// type should only be used for its `Display` implementation. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let c = Cookie::build(("my name", "this; value%?")).secure(true).build(); /// assert_eq!(&c.stripped().to_string(), "my name=this; value%?"); #[cfg_attr(feature = "percent-encode", doc = r##" // Note: `encoded()` is only available when `percent-encode` is enabled. assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%25%3F; Secure"); assert_eq!(&c.stripped().encoded().to_string(), "my%20name=this%3B%20value%25%3F"); assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%25%3F"); "##)] /// ``` pub struct Display<'a, 'c: 'a> { cookie: &'a Cookie<'c>, #[cfg(feature = "percent-encode")] encode: bool, strip: bool, } impl<'a, 'c: 'a> fmt::Display for Display<'a, 'c> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { #[cfg(feature = "percent-encode")] { if self.encode { let name = encoding::encode(self.cookie.name()); let value = encoding::encode(self.cookie.value()); write!(f, "{}={}", name, value)?; } else { write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; } } #[cfg(not(feature = "percent-encode"))] { write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; } match self.strip { true => Ok(()), false => self.cookie.fmt_parameters(f) } } } impl<'a, 'c> Display<'a, 'c> { #[cfg(feature = "percent-encode")] fn new_encoded(cookie: &'a Cookie<'c>) -> Self { Display { cookie, strip: false, encode: true } } fn new_stripped(cookie: &'a Cookie<'c>) -> Self { Display { cookie, strip: true, #[cfg(feature = "percent-encode")] encode: false } } /// Percent-encode the name and value pair. #[inline] #[cfg(feature = "percent-encode")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] pub fn encoded(mut self) -> Self { self.encode = true; self } /// Only display the name and value. #[inline] pub fn stripped(mut self) -> Self { self.strip = true; self } } impl<'c> fmt::Display for Cookie<'c> { /// Formats the cookie `self` as a `Set-Cookie` header value. /// /// Does _not_ percent-encode any values. To percent-encode, use /// [`Cookie::encoded()`]. /// /// # Example /// /// ```rust /// use cookie::Cookie; /// /// let mut cookie = Cookie::build(("foo", "bar")).path("/"); /// assert_eq!(cookie.to_string(), "foo=bar; Path=/"); /// ``` fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}={}", self.name(), self.value())?; self.fmt_parameters(f) } } impl FromStr for Cookie<'static> { type Err = ParseError; fn from_str(s: &str) -> Result, ParseError> { Cookie::parse(s).map(|c| c.into_owned()) } } impl<'a, 'b> PartialEq> for Cookie<'a> { fn eq(&self, other: &Cookie<'b>) -> bool { let so_far_so_good = self.name() == other.name() && self.value() == other.value() && self.http_only() == other.http_only() && self.secure() == other.secure() && self.max_age() == other.max_age() && self.expires() == other.expires(); if !so_far_so_good { return false; } match (self.path(), other.path()) { (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} (None, None) => {} _ => return false, }; match (self.domain(), other.domain()) { (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} (None, None) => {} _ => return false, }; true } } impl<'a> From<&'a str> for Cookie<'a> { fn from(name: &'a str) -> Self { Cookie::new(name, "") } } impl From for Cookie<'static> { fn from(name: String) -> Self { Cookie::new(name, "") } } impl<'a> From> for Cookie<'a> { fn from(name: Cow<'a, str>) -> Self { Cookie::new(name, "") } } impl<'a, N, V> From<(N, V)> for Cookie<'a> where N: Into>, V: Into> { fn from((name, value): (N, V)) -> Self { Cookie::new(name, value) } } impl<'a> From> for Cookie<'a> { fn from(builder: CookieBuilder<'a>) -> Self { builder.build() } } impl<'a> AsRef> for Cookie<'a> { fn as_ref(&self) -> &Cookie<'a> { self } } impl<'a> AsMut> for Cookie<'a> { fn as_mut(&mut self) -> &mut Cookie<'a> { self } } #[cfg(test)] mod tests { use crate::{Cookie, SameSite, parse::parse_date}; use time::{Duration, OffsetDateTime}; #[test] fn format() { let cookie = Cookie::new("foo", "bar"); assert_eq!(&cookie.to_string(), "foo=bar"); let cookie = Cookie::build(("foo", "bar")).http_only(true); assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); let cookie = Cookie::build(("foo", "bar")).max_age(Duration::seconds(10)); assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); let cookie = Cookie::build(("foo", "bar")).secure(true); assert_eq!(&cookie.to_string(), "foo=bar; Secure"); let cookie = Cookie::build(("foo", "bar")).path("/"); assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); let cookie = Cookie::build(("foo", "bar")).domain("www.rust-lang.org"); assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); let cookie = Cookie::build(("foo", "bar")).domain(".rust-lang.org"); assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org"); let cookie = Cookie::build(("foo", "bar")).domain("rust-lang.org"); assert_eq!(&cookie.to_string(), "foo=bar; Domain=rust-lang.org"); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; let expires = parse_date(time_str, &crate::parse::FMT1).unwrap(); let cookie = Cookie::build(("foo", "bar")).expires(expires); assert_eq!(&cookie.to_string(), "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT"); let cookie = Cookie::build(("foo", "bar")).same_site(SameSite::Strict); assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); let cookie = Cookie::build(("foo", "bar")).same_site(SameSite::Lax); assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); let mut cookie = Cookie::build(("foo", "bar")).same_site(SameSite::None).build(); assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); cookie.set_same_site(None); assert_eq!(&cookie.to_string(), "foo=bar"); let mut c = Cookie::build(("foo", "bar")).same_site(SameSite::None).secure(false).build(); assert_eq!(&c.to_string(), "foo=bar; SameSite=None"); c.set_secure(true); assert_eq!(&c.to_string(), "foo=bar; SameSite=None; Secure"); } #[test] #[ignore] fn format_date_wraps() { let expires = OffsetDateTime::UNIX_EPOCH + Duration::MAX; let cookie = Cookie::build(("foo", "bar")).expires(expires); assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); let expires = time::macros::datetime!(9999-01-01 0:00 UTC) + Duration::days(1000); let cookie = Cookie::build(("foo", "bar")).expires(expires); assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); } #[test] fn cookie_string_long_lifetimes() { let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); let (name, value, path, domain) = { // Create a cookie passing a slice let c = Cookie::parse(cookie_string.as_str()).unwrap(); (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) }; assert_eq!(name, Some("bar")); assert_eq!(value, Some("baz")); assert_eq!(path, Some("/subdir")); assert_eq!(domain, Some("crates.io")); } #[test] fn owned_cookie_string() { let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); let (name, value, path, domain) = { // Create a cookie passing an owned string let c = Cookie::parse(cookie_string).unwrap(); (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) }; assert_eq!(name, None); assert_eq!(value, None); assert_eq!(path, None); assert_eq!(domain, None); } #[test] fn owned_cookie_struct() { let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io"; let (name, value, path, domain) = { // Create an owned cookie let c = Cookie::parse(cookie_string).unwrap().into_owned(); (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) }; assert_eq!(name, None); assert_eq!(value, None); assert_eq!(path, None); assert_eq!(domain, None); } #[test] #[cfg(feature = "percent-encode")] fn format_encoded() { let cookie = Cookie::new("foo !%?=", "bar;;, a"); let cookie_str = cookie.encoded().to_string(); assert_eq!(&cookie_str, "foo%20!%25%3F%3D=bar%3B%3B%2C%20a"); let cookie = Cookie::parse_encoded(cookie_str).unwrap(); assert_eq!(cookie.name_value(), ("foo !%?=", "bar;;, a")); } #[test] fn split_parse() { let cases = [ ("", vec![]), (";;", vec![]), ("name=value", vec![("name", "value")]), ("a=%20", vec![("a", "%20")]), ("a=d#$%^&*()_", vec![("a", "d#$%^&*()_")]), (" name=value ", vec![("name", "value")]), ("name=value ", vec![("name", "value")]), ("name=value;;other=key", vec![("name", "value"), ("other", "key")]), ("name=value; ;other=key", vec![("name", "value"), ("other", "key")]), ("name=value ; ;other=key", vec![("name", "value"), ("other", "key")]), ("name=value ; ; other=key", vec![("name", "value"), ("other", "key")]), ("name=value ; ; other=key ", vec![("name", "value"), ("other", "key")]), ("name=value ; ; other=key;; ", vec![("name", "value"), ("other", "key")]), (";name=value ; ; other=key ", vec![("name", "value"), ("other", "key")]), (";a=1 ; ; b=2 ", vec![("a", "1"), ("b", "2")]), (";a=1 ; ; b= ", vec![("a", "1"), ("b", "")]), (";a=1 ; ; =v ; c=", vec![("a", "1"), ("c", "")]), (" ; a=1 ; ; =v ; ;;c=", vec![("a", "1"), ("c", "")]), (" ; a=1 ; ; =v ; ;;c=== ", vec![("a", "1"), ("c", "==")]), ]; for (string, expected) in cases { let actual: Vec<_> = Cookie::split_parse(string) .filter_map(|parse| parse.ok()) .map(|c| (c.name_raw().unwrap(), c.value_raw().unwrap())) .collect(); assert_eq!(expected, actual); } } #[test] #[cfg(feature = "percent-encode")] fn split_parse_encoded() { let cases = [ ("", vec![]), (";;", vec![]), ("name=val%20ue", vec![("name", "val ue")]), ("foo%20!%25%3F%3D=bar%3B%3B%2C%20a", vec![("foo !%?=", "bar;;, a")]), ( "name=val%20ue ; ; foo%20!%25%3F%3D=bar%3B%3B%2C%20a", vec![("name", "val ue"), ("foo !%?=", "bar;;, a")] ), ]; for (string, expected) in cases { let cookies: Vec<_> = Cookie::split_parse_encoded(string) .filter_map(|parse| parse.ok()) .collect(); let actual: Vec<_> = cookies.iter() .map(|c| c.name_value()) .collect(); assert_eq!(expected, actual); } } } cookie-0.18.0/src/parse.rs000064400000000000000000000477301046102023000134450ustar 00000000000000use std::borrow::Cow; use std::error::Error; use std::convert::{From, TryFrom}; use std::str::Utf8Error; use std::fmt; #[allow(unused_imports, deprecated)] use std::ascii::AsciiExt; #[cfg(feature = "percent-encode")] use percent_encoding::percent_decode; use time::{PrimitiveDateTime, Duration, OffsetDateTime}; use time::{parsing::Parsable, macros::format_description, format_description::FormatItem}; use crate::{Cookie, SameSite, CookieStr}; // The three formats spec'd in http://tools.ietf.org/html/rfc2616#section-3.3.1. // Additional ones as encountered in the real world. pub static FMT1: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT"); pub static FMT2: &[FormatItem<'_>] = format_description!("[weekday], [day]-[month repr:short]-[year repr:last_two] [hour]:[minute]:[second] GMT"); pub static FMT3: &[FormatItem<'_>] = format_description!("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]"); pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT"); /// Enum corresponding to a parsing error. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum ParseError { /// The cookie did not contain a name/value pair. MissingPair, /// The cookie's name was empty. EmptyName, /// Decoding the cookie's name or value resulted in invalid UTF-8. Utf8Error(Utf8Error), } impl ParseError { /// Returns a description of this error as a string pub fn as_str(&self) -> &'static str { match *self { ParseError::MissingPair => "the cookie is missing a name/value pair", ParseError::EmptyName => "the cookie's name is empty", ParseError::Utf8Error(_) => { "decoding the cookie's name or value resulted in invalid UTF-8" } } } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.as_str()) } } impl From for ParseError { fn from(error: Utf8Error) -> ParseError { ParseError::Utf8Error(error) } } impl Error for ParseError { fn description(&self) -> &str { self.as_str() } } #[cfg(feature = "percent-encode")] fn name_val_decoded( name: &str, val: &str ) -> Result, CookieStr<'static>)>, ParseError> { let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; if let (&Cow::Borrowed(_), &Cow::Borrowed(_)) = (&decoded_name, &decoded_value) { Ok(None) } else { let name = CookieStr::Concrete(Cow::Owned(decoded_name.into())); let val = CookieStr::Concrete(Cow::Owned(decoded_value.into())); Ok(Some((name, val))) } } #[cfg(not(feature = "percent-encode"))] fn name_val_decoded( _: &str, _: &str ) -> Result, CookieStr<'static>)>, ParseError> { unreachable!("This function should never be called with 'percent-encode' disabled!") } // This function does the real parsing but _does not_ set the `cookie_string` in // the returned cookie object. This only exists so that the borrow to `s` is // returned at the end of the call, allowing the `cookie_string` field to be // set in the outer `parse` function. fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { let mut attributes = s.split(';'); // Determine the name = val. let key_value = attributes.next().expect("first str::split().next() returns Some"); let (name, value) = match key_value.find('=') { Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()), None => return Err(ParseError::MissingPair) }; if name.is_empty() { return Err(ParseError::EmptyName); } // If there is nothing to decode, or we're not decoding, use indexes. let indexed_names = |s, name, value| { let name = CookieStr::indexed(name, s).expect("name sub"); let value = CookieStr::indexed(value, s).expect("value sub"); (name, value) }; // Create a cookie with all of the defaults. We'll fill things in while we // iterate through the parameters below. let (name, value) = if decode { match name_val_decoded(name, value)? { Some((name, value)) => (name, value), None => indexed_names(s, name, value) } } else { indexed_names(s, name, value) }; let mut cookie: Cookie<'c> = Cookie { name, value, cookie_string: None, expires: None, max_age: None, domain: None, path: None, secure: None, http_only: None, same_site: None }; for attr in attributes { let (key, value) = match attr.find('=') { Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())), None => (attr.trim(), None), }; match (&*key.to_ascii_lowercase(), value) { ("secure", _) => cookie.secure = Some(true), ("httponly", _) => cookie.http_only = Some(true), ("max-age", Some(mut v)) => cookie.max_age = { let is_negative = v.starts_with('-'); if is_negative { v = &v[1..]; } if !v.chars().all(|d| d.is_digit(10)) { continue } // From RFC 6265 5.2.2: neg values indicate that the earliest // expiration should be used, so set the max age to 0 seconds. if is_negative { Some(Duration::ZERO) } else { Some(v.parse::() .map(Duration::seconds) .unwrap_or_else(|_| Duration::seconds(i64::max_value()))) } }, ("domain", Some(d)) if !d.is_empty() => { cookie.domain = Some(CookieStr::indexed(d, s).expect("domain sub")); }, ("path", Some(v)) => { cookie.path = Some(CookieStr::indexed(v, s).expect("path sub")); }, ("samesite", Some(v)) => { if v.eq_ignore_ascii_case("strict") { cookie.same_site = Some(SameSite::Strict); } else if v.eq_ignore_ascii_case("lax") { cookie.same_site = Some(SameSite::Lax); } else if v.eq_ignore_ascii_case("none") { cookie.same_site = Some(SameSite::None); } else { // We do nothing here, for now. When/if the `SameSite` // attribute becomes standard, the spec says that we should // ignore this cookie, i.e, fail to parse it, when an // invalid value is passed in. The draft is at // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. } } ("expires", Some(v)) => { let tm = parse_date(v, &FMT1) .or_else(|_| parse_date(v, &FMT2)) .or_else(|_| parse_date(v, &FMT3)) .or_else(|_| parse_date(v, &FMT4)); // .or_else(|_| parse_date(v, &FMT5)); if let Ok(time) = tm { cookie.expires = Some(time.into()) } } _ => { // We're going to be permissive here. If we have no idea what // this is, then it's something nonstandard. We're not going to // store it (because it's not compliant), but we're also not // going to emit an error. } } } Ok(cookie) } pub(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, ParseError> where S: Into> { let s = cow.into(); let mut cookie = parse_inner(&s, decode)?; cookie.cookie_string = Some(s); Ok(cookie) } pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result { // Parse. Handle "abbreviated" dates like Chromium. See cookie#162. let mut date = format.parse(s.as_bytes())?; if let Some(y) = date.year().or_else(|| date.year_last_two().map(|v| v as i32)) { let offset = match y { 0..=68 => 2000, 69..=99 => 1900, _ => 0, }; date.set_year(y + offset); } Ok(PrimitiveDateTime::try_from(date)?.assume_utc()) } #[cfg(test)] mod tests { use super::parse_date; use crate::{Cookie, SameSite}; use time::Duration; macro_rules! assert_eq_parse { ($string:expr, $expected:expr) => ( let cookie = match Cookie::parse($string) { Ok(cookie) => cookie, Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e) }; assert_eq!(cookie, $expected); ) } macro_rules! assert_ne_parse { ($string:expr, $expected:expr) => ( let cookie = match Cookie::parse($string) { Ok(cookie) => cookie, Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e) }; assert_ne!(cookie, $expected); ) } #[test] fn parse_same_site() { let expected = Cookie::build(("foo", "bar")).same_site(SameSite::Lax); assert_eq_parse!("foo=bar; SameSite=Lax", expected); assert_eq_parse!("foo=bar; SameSite=lax", expected); assert_eq_parse!("foo=bar; SameSite=LAX", expected); assert_eq_parse!("foo=bar; samesite=Lax", expected); assert_eq_parse!("foo=bar; SAMESITE=Lax", expected); let expected = Cookie::build(("foo", "bar")).same_site(SameSite::Strict); assert_eq_parse!("foo=bar; SameSite=Strict", expected); assert_eq_parse!("foo=bar; SameSITE=Strict", expected); assert_eq_parse!("foo=bar; SameSite=strict", expected); assert_eq_parse!("foo=bar; SameSite=STrICT", expected); assert_eq_parse!("foo=bar; SameSite=STRICT", expected); let expected = Cookie::build(("foo", "bar")).same_site(SameSite::None); assert_eq_parse!("foo=bar; SameSite=None", expected); assert_eq_parse!("foo=bar; SameSITE=none", expected); assert_eq_parse!("foo=bar; SameSite=NOne", expected); assert_eq_parse!("foo=bar; SameSite=nOne", expected); } #[test] fn parse() { assert!(Cookie::parse("bar").is_err()); assert!(Cookie::parse("=bar").is_err()); assert!(Cookie::parse(" =bar").is_err()); assert!(Cookie::parse("foo=").is_ok()); let expected = Cookie::new("foo", "bar=baz"); assert_eq_parse!("foo=bar=baz", expected); let expected = Cookie::new("foo", "\"\"bar\"\""); assert_eq_parse!("foo=\"\"bar\"\"", expected); let expected = Cookie::new("foo", "\"bar"); assert_eq_parse!("foo= \"bar", expected); assert_eq_parse!("foo=\"bar ", expected); assert_ne_parse!("foo=\"\"bar\"", expected); assert_ne_parse!("foo=\"\"bar \"", expected); assert_ne_parse!("foo=\"\"bar \" ", expected); let expected = Cookie::new("foo", "bar\""); assert_eq_parse!("foo=bar\"", expected); assert_ne_parse!("foo=\"bar\"\"", expected); assert_ne_parse!("foo=\" bar\"\"", expected); assert_ne_parse!("foo=\" bar\" \" ", expected); let mut expected = Cookie::new("foo", "bar"); assert_eq_parse!("foo=bar", expected); assert_eq_parse!("foo = bar", expected); assert_eq_parse!(" foo=bar ", expected); assert_eq_parse!(" foo=bar ;Domain=", expected); assert_eq_parse!(" foo=bar ;Domain= ", expected); assert_eq_parse!(" foo=bar ;Ignored", expected); assert_ne_parse!("foo=\"bar\"", expected); assert_ne_parse!(" foo=\"bar \" ", expected); let mut unexpected = Cookie::build(("foo", "bar")).http_only(false).build(); assert_ne_parse!(" foo=bar ;HttpOnly", unexpected); assert_ne_parse!(" foo=bar; httponly", unexpected); expected.set_http_only(true); assert_eq_parse!(" foo=bar ;HttpOnly", expected); assert_eq_parse!(" foo=bar ;httponly", expected); assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected); assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected); expected.set_secure(true); assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected); unexpected.set_http_only(true); unexpected.set_secure(true); assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected); unexpected.set_secure(false); assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); expected.set_max_age(Duration::ZERO); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); expected.set_max_age(Duration::minutes(1)); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected); expected.set_max_age(Duration::seconds(4)); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected); unexpected.set_secure(true); unexpected.set_max_age(Duration::minutes(1)); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected); expected.set_path("/"); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected); expected.set_path("/foo"); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected); assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected); unexpected.set_max_age(Duration::seconds(4)); unexpected.set_path("/bar"); assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected); assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected); expected.set_domain("www.foo.com"); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=www.foo.com", expected); expected.set_domain("foo.com"); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=FOO.COM", expected); expected.set_domain(".foo.com"); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=.foo.com", expected); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=.FOO.COM", expected); unexpected.set_path("/foo"); unexpected.set_domain("bar.com"); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com", unexpected); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=FOO.COM", unexpected); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; let expires = parse_date(time_str, &super::FMT1).unwrap(); expected.set_expires(expires); assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected); unexpected.set_domain("foo.com"); let bad_expires = parse_date(time_str, &super::FMT1).unwrap(); expected.set_expires(bad_expires); assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected); } #[test] fn parse_abbreviated_years() { let cookie_str = "foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2020); let cookie_str = "foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2068); let cookie_str = "foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 1969); let cookie_str = "foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 1999); let cookie_str = "foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT"; let cookie = Cookie::parse(cookie_str).unwrap(); assert_eq!(cookie.expires_datetime().unwrap().year(), 2069); } #[test] fn parse_variant_date_fmts() { let cookie_str = "foo=bar; expires=Sun, 06 Nov 1994 08:49:37 GMT"; Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); let cookie_str = "foo=bar; expires=Sunday, 06-Nov-94 08:49:37 GMT"; Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); let cookie_str = "foo=bar; expires=Sun Nov 6 08:49:37 1994"; Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); } #[test] fn parse_very_large_max_ages() { let mut expected = Cookie::build(("foo", "bar")) .max_age(Duration::seconds(i64::max_value())) .build(); let string = format!("foo=bar; Max-Age={}", 1u128 << 100); assert_eq_parse!(&string, expected); expected.set_max_age(Duration::seconds(0)); assert_eq_parse!("foo=bar; Max-Age=-129", expected); let string = format!("foo=bar; Max-Age=-{}", 1u128 << 100); assert_eq_parse!(&string, expected); let string = format!("foo=bar; Max-Age=-{}", i64::max_value()); assert_eq_parse!(&string, expected); let string = format!("foo=bar; Max-Age={}", i64::max_value()); expected.set_max_age(Duration::seconds(i64::max_value())); assert_eq_parse!(&string, expected); } #[test] fn odd_characters() { let expected = Cookie::new("foo", "b%2Fr"); assert_eq_parse!("foo=b%2Fr", expected); } #[test] #[cfg(feature = "percent-encode")] fn odd_characters_encoded() { let expected = Cookie::new("foo", "b/r"); let cookie = match Cookie::parse_encoded("foo=b%2Fr") { Ok(cookie) => cookie, Err(e) => panic!("Failed to parse: {:?}", e) }; assert_eq!(cookie, expected); } #[test] fn do_not_panic_on_large_max_ages() { let max_seconds = Duration::MAX.whole_seconds(); let expected = Cookie::build(("foo", "bar")) .max_age(Duration::seconds(max_seconds)); let too_many_seconds = (max_seconds as u64) + 1; assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected); } } cookie-0.18.0/src/prefix.rs000064400000000000000000000314421046102023000136210ustar 00000000000000use std::marker::PhantomData; use std::borrow::{Borrow, BorrowMut, Cow}; use crate::{CookieJar, Cookie}; /// A child jar that automatically [prefixes](Prefix) cookies. /// /// Obtained via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`]. /// /// This jar implements the [HTTP RFC6265 draft] "cookie prefixes" extension by /// automatically adding and removing a specified [`Prefix`] from cookies that /// are added and retrieved from this jar, respectively. Additionally, upon /// being added to this jar, cookies are automatically made to /// [conform](Prefix::conform()) to the corresponding prefix's specifications. /// /// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and /// definition are subject to change. /// /// [HTTP RFC6265 draft]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes pub struct PrefixedJar { parent: J, _prefix: PhantomData P>, } /// The [`"__Host-"`] cookie [`Prefix`]. /// /// See [`Prefix`] and [`PrefixedJar`] for usage details. /// /// [`"__Host-"`]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix pub struct Host; /// The [`"__Secure-"`] cookie [`Prefix`]. /// /// See [`Prefix`] and [`PrefixedJar`] for usage details. /// /// [`"__Secure-"`]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix pub struct Secure; /// Trait identifying [HTTP RFC6265 draft] cookie prefixes. /// /// A [`Prefix`] can be applied to cookies via a child [`PrefixedJar`], itself /// obtainable via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`]. /// Cookies added/retrieved to/from these child jars have the corresponding /// [prefix](Prefix::conform()) automatically prepended/removed as needed. /// Additionally, added cookies are automatically make to /// [conform](Prefix::conform()). /// /// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and /// definition are subject to change. /// /// [HTTP RFC6265 draft]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes pub trait Prefix: private::Sealed { /// The prefix string to prepend. /// /// See [`Host::PREFIX`] and [`Secure::PREFIX`] for specifics. const PREFIX: &'static str; /// Alias to [`Host`]. #[allow(non_upper_case_globals)] const Host: Host = Host; /// Alias to [`Secure`]. #[allow(non_upper_case_globals)] const Secure: Secure = Secure; /// Modify `cookie` so it conforms to the requirements of `self`. /// /// See [`Host::conform()`] and [`Secure::conform()`] for specifics. // // This is the only required method. Everything else is shared across // implementations via the default implementations below and should not be // implemented. fn conform(cookie: Cookie<'_>) -> Cookie<'_>; /// Returns a string with `name` prefixed with `self`. #[doc(hidden)] #[inline(always)] fn prefixed_name(name: &str) -> String { format!("{}{}", Self::PREFIX, name) } /// Prefix `cookie`'s name with `Self`. #[doc(hidden)] fn prefix(mut cookie: Cookie<'_>) -> Cookie<'_> { use crate::CookieStr; cookie.name = CookieStr::Concrete(match cookie.name { CookieStr::Concrete(Cow::Owned(mut string)) => { string.insert_str(0, Self::PREFIX); string.into() } _ => Self::prefixed_name(cookie.name()).into(), }); cookie } /// Remove the prefix `Self` from `cookie`'s name and return it. /// /// If the prefix isn't in `cookie`, the cookie is returned unmodified. This /// method is expected to be called only when `cookie`'s name is known to /// contain the prefix. #[doc(hidden)] fn clip(mut cookie: Cookie<'_>) -> Cookie<'_> { use std::borrow::Cow::*; use crate::CookieStr::*; if !cookie.name().starts_with(Self::PREFIX) { return cookie; } let len = Self::PREFIX.len(); cookie.name = match cookie.name { Indexed(i, j) => Indexed(i + len, j), Concrete(Borrowed(v)) => Concrete(Borrowed(&v[len..])), Concrete(Owned(v)) => Concrete(Owned(v[len..].to_string())), }; cookie } /// Prefix and _conform_ `cookie`: prefix `cookie` with `Self` and make it /// conform to the required specification by modifying it. #[inline] #[doc(hidden)] fn apply(cookie: Cookie<'_>) -> Cookie<'_> { Self::conform(Self::prefix(cookie)) } } impl PrefixedJar { #[inline(always)] pub(crate) fn new(parent: J) -> Self { Self { parent, _prefix: PhantomData } } } impl> PrefixedJar { /// Fetches the `Cookie` inside this jar with the prefix `P` and removes the /// prefix before returning it. If the cookie isn't found, returns `None`. /// /// See [`CookieJar::prefixed()`] for more examples. /// /// # Example /// /// ```rust /// use cookie::CookieJar; /// use cookie::prefix::{Host, Secure}; /// /// // Add a `Host` prefixed cookie. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Host).add(("h0st", "value")); /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st"); /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value"); /// ``` pub fn get(&self, name: &str) -> Option> { self.parent.borrow() .get(&P::prefixed_name(name)) .map(|c| P::clip(c.clone())) } } impl> PrefixedJar { /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`, /// and the cookie's attributes are made to [`conform`](Prefix::conform()). /// /// See [`CookieJar::prefixed_mut()`] for more examples. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// use cookie::prefix::{Host, Secure}; /// /// // Add a `Host` prefixed cookie. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Secure).add(Cookie::build(("name", "value")).secure(false)); /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().value(), "value"); /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().secure(), Some(true)); /// ``` pub fn add>>(&mut self, cookie: C) { self.parent.borrow_mut().add(P::apply(cookie.into())); } /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`, /// and the cookie's attributes are made to [`conform`](Prefix::conform()). /// /// Adding an original cookie does not affect the [`CookieJar::delta()`] /// computation. This method is intended to be used to seed the cookie jar /// with cookies. For accurate `delta` computations, this method should not /// be called after calling `remove`. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// use cookie::prefix::{Host, Secure}; /// /// // Add a `Host` prefixed cookie. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Secure).add_original(("name", "value")); /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 0); /// ``` pub fn add_original>>(&mut self, cookie: C) { self.parent.borrow_mut().add_original(P::apply(cookie.into())); } /// Removes `cookie` from the parent jar. /// /// The cookie's name is prefixed with `P`, and the cookie's attributes are /// made to [`conform`](Prefix::conform()) before attempting to remove the /// cookie. For correct removal, the passed in `cookie` must contain the /// same `path` and `domain` as the cookie that was initially set. /// /// # Example /// /// ```rust /// use cookie::{Cookie, CookieJar}; /// use cookie::prefix::{Host, Secure}; /// /// let mut jar = CookieJar::new(); /// let mut prefixed_jar = jar.prefixed_mut(Host); /// /// prefixed_jar.add(("name", "value")); /// assert!(prefixed_jar.get("name").is_some()); /// /// prefixed_jar.remove("name"); /// assert!(prefixed_jar.get("name").is_none()); /// ``` pub fn remove>>(&mut self, cookie: C) { self.parent.borrow_mut().remove(P::apply(cookie.into())); } } impl Prefix for Host { /// The [`"__Host-"` prefix] string. /// /// [`"__Host-"` prefix]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix const PREFIX: &'static str = "__Host-"; /// Modify `cookie` so it conforms to the prefix's requirements. /// /// **Note: this method is called automatically by [`PrefixedJar`]. It _does /// not need to_ and _should not_ be called manually under normal /// circumstances.** /// /// According to [RFC 6265bis-12 §4.1.3.2]: /// /// ```text /// If a cookie's name begins with a case-sensitive match for the string /// __Host-, then the cookie will have been set with a Secure attribute, /// a Path attribute with a value of /, and no Domain attribute. /// ``` /// /// As such, to make a cookie conforn, this method: /// /// * Sets [`secure`](Cookie::set_secure()) to `true`. /// * Sets the [`path`](Cookie::set_path()) to `"/"`. /// * Removes the [`domain`](Cookie::unset_domain()), if any. /// /// [RFC 6265bis-12 §4.1.3.2]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, prefix::Host}; /// /// // A cookie with some non-conformant properties. /// let cookie = Cookie::build(("name", "some-value")) /// .secure(false) /// .path("/foo/bar") /// .domain("rocket.rs") /// .http_only(true); /// /// // Add the cookie to the jar. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Host).add(cookie); /// /// // Fetch the cookie: notice it's been made to conform. /// let cookie = jar.prefixed(Host).get("name").unwrap(); /// assert_eq!(cookie.name(), "name"); /// assert_eq!(cookie.value(), "some-value"); /// assert_eq!(cookie.secure(), Some(true)); /// assert_eq!(cookie.path(), Some("/")); /// assert_eq!(cookie.domain(), None); /// assert_eq!(cookie.http_only(), Some(true)); /// ``` fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> { cookie.set_secure(true); cookie.set_path("/"); cookie.unset_domain(); cookie } } impl Prefix for Secure { /// The [`"__Secure-"` prefix] string. /// /// [`"__Secure-"` prefix]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix const PREFIX: &'static str = "__Secure-"; /// Modify `cookie` so it conforms to the prefix's requirements. /// /// **Note: this method is called automatically by [`PrefixedJar`]. It _does /// not need to_ and _should not_ be called manually under normal /// circumstances.** /// /// According to [RFC 6265bis-12 §4.1.3.1]: /// /// ```text /// If a cookie's name begins with a case-sensitive match for the string /// __Secure-, then the cookie will have been set with a Secure /// attribute. /// ``` /// /// As such, to make a cookie conforn, this method: /// /// * Sets [`secure`](Cookie::set_secure()) to `true`. /// /// [RFC 6265bis-12 §4.1.3.1]: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, prefix::Secure}; /// /// // A cookie with some non-conformant properties. /// let cookie = Cookie::build(("name", "some-value")) /// .secure(false) /// .path("/guide") /// .domain("rocket.rs") /// .http_only(true); /// /// // Add the cookie to the jar. /// let mut jar = CookieJar::new(); /// jar.prefixed_mut(Secure).add(cookie); /// /// // Fetch the cookie: notice it's been made to conform. /// let cookie = jar.prefixed(Secure).get("name").unwrap(); /// assert_eq!(cookie.name(), "name"); /// assert_eq!(cookie.value(), "some-value"); /// assert_eq!(cookie.secure(), Some(true)); /// assert_eq!(cookie.path(), Some("/guide")); /// assert_eq!(cookie.domain(), Some("rocket.rs")); /// assert_eq!(cookie.http_only(), Some(true)); /// ``` fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> { cookie.set_secure(true); cookie } } mod private { pub trait Sealed {} impl Sealed for super::Host {} impl Sealed for super::Secure {} } cookie-0.18.0/src/same_site.rs000064400000000000000000000064501046102023000142760ustar 00000000000000//! This module contains types that represent cookie properties that are not yet //! standardized. That is, _draft_ features. use std::fmt; /// The `SameSite` cookie attribute. /// /// A cookie with a `SameSite` attribute is imposed restrictions on when it is /// sent to the origin server in a cross-site request. If the `SameSite` /// attribute is "Strict", then the cookie is never sent in cross-site requests. /// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site /// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`. /// If the `SameSite` attribute is "None", the cookie is sent in all cross-site /// requests if the "Secure" flag is also set, otherwise the cookie is ignored. /// This library automatically sets the "Secure" flag on cookies when /// `same_site` is set to `SameSite::None` as long as `secure` is not explicitly /// set to `false`. /// /// If the `SameSite` attribute is not present (by not setting `SameSite` /// initally or passing `None` to [`Cookie::set_same_site()`]), then the cookie /// will be sent as normal. /// /// **Note:** This cookie attribute is an [HTTP draft]! Its meaning and /// definition are subject to change. /// /// [`Cookie::set_same_site()`]: crate::Cookie::set_same_site() /// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SameSite { /// The "Strict" `SameSite` attribute. Strict, /// The "Lax" `SameSite` attribute. Lax, /// The "None" `SameSite` attribute. None } impl SameSite { /// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise. /// /// # Example /// /// ```rust /// use cookie::SameSite; /// /// let strict = SameSite::Strict; /// assert!(strict.is_strict()); /// assert!(!strict.is_lax()); /// assert!(!strict.is_none()); /// ``` #[inline] pub fn is_strict(&self) -> bool { match *self { SameSite::Strict => true, SameSite::Lax | SameSite::None => false, } } /// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise. /// /// # Example /// /// ```rust /// use cookie::SameSite; /// /// let lax = SameSite::Lax; /// assert!(lax.is_lax()); /// assert!(!lax.is_strict()); /// assert!(!lax.is_none()); /// ``` #[inline] pub fn is_lax(&self) -> bool { match *self { SameSite::Lax => true, SameSite::Strict | SameSite::None => false, } } /// Returns `true` if `self` is `SameSite::None` and `false` otherwise. /// /// # Example /// /// ```rust /// use cookie::SameSite; /// /// let none = SameSite::None; /// assert!(none.is_none()); /// assert!(!none.is_lax()); /// assert!(!none.is_strict()); /// ``` #[inline] pub fn is_none(&self) -> bool { match *self { SameSite::None => true, SameSite::Lax | SameSite::Strict => false } } } impl fmt::Display for SameSite { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { SameSite::Strict => write!(f, "Strict"), SameSite::Lax => write!(f, "Lax"), SameSite::None => write!(f, "None"), } } } cookie-0.18.0/src/secure/key.rs000064400000000000000000000222251046102023000144010ustar 00000000000000use std::convert::TryFrom; const SIGNING_KEY_LEN: usize = 32; const ENCRYPTION_KEY_LEN: usize = 32; const COMBINED_KEY_LENGTH: usize = SIGNING_KEY_LEN + ENCRYPTION_KEY_LEN; // Statically ensure the numbers above are in-sync. #[cfg(feature = "signed")] const_assert!(crate::secure::signed::KEY_LEN == SIGNING_KEY_LEN); #[cfg(feature = "private")] const_assert!(crate::secure::private::KEY_LEN == ENCRYPTION_KEY_LEN); /// A cryptographic master key for use with `Signed` and/or `Private` jars. /// /// This structure encapsulates secure, cryptographic keys for use with both /// [`PrivateJar`](crate::PrivateJar) and [`SignedJar`](crate::SignedJar). A /// single instance of a `Key` can be used for both a `PrivateJar` and a /// `SignedJar` simultaneously with no notable security implications. #[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] #[derive(Clone)] pub struct Key([u8; COMBINED_KEY_LENGTH /* SIGNING | ENCRYPTION */]); impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { use subtle::ConstantTimeEq; self.0.ct_eq(&other.0).into() } } impl std::fmt::Debug for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Key").finish() } } impl Key { // An empty key structure, to be filled. const fn zero() -> Self { Key([0; COMBINED_KEY_LENGTH]) } /// Creates a new `Key` from a 512-bit cryptographically random string. /// /// The supplied key must be at least 512-bits (64 bytes). For security, the /// master key _must_ be cryptographically random. /// /// # Panics /// /// Panics if `key` is less than 64 bytes in length. /// /// For a non-panicking version, use [`Key::try_from()`] or generate a key with /// [`Key::generate()`] or [`Key::try_generate()`]. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// # /* /// let key = { /* a cryptographically random key >= 64 bytes */ }; /// # */ /// # let key: &Vec = &(0..64).collect(); /// /// let key = Key::from(key); /// ``` #[inline] pub fn from(key: &[u8]) -> Key { Key::try_from(key).unwrap() } /// Derives new signing/encryption keys from a master key. /// /// The master key must be at least 256-bits (32 bytes). For security, the /// master key _must_ be cryptographically random. The keys are derived /// deterministically from the master key. /// /// # Panics /// /// Panics if `key` is less than 32 bytes in length. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// # /* /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; /// # */ /// # let master_key: &Vec = &(0..32).collect(); /// /// let key = Key::derive_from(master_key); /// ``` #[cfg(feature = "key-expansion")] #[cfg_attr(all(nightly, doc), doc(cfg(feature = "key-expansion")))] pub fn derive_from(master_key: &[u8]) -> Self { if master_key.len() < 32 { panic!("bad master key length: expected >= 32 bytes, found {}", master_key.len()); } // Expand the master key into two HKDF generated keys. const KEYS_INFO: &[u8] = b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"; let mut both_keys = [0; COMBINED_KEY_LENGTH]; let hk = hkdf::Hkdf::::from_prk(master_key).expect("key length prechecked"); hk.expand(KEYS_INFO, &mut both_keys).expect("expand into keys"); Key::from(&both_keys) } /// Generates signing/encryption keys from a secure, random source. Keys are /// generated nondeterministically. /// /// # Panics /// /// Panics if randomness cannot be retrieved from the operating system. See /// [`Key::try_generate()`] for a non-panicking version. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// let key = Key::generate(); /// ``` pub fn generate() -> Key { Self::try_generate().expect("failed to generate `Key` from randomness") } /// Attempts to generate signing/encryption keys from a secure, random /// source. Keys are generated nondeterministically. If randomness cannot be /// retrieved from the underlying operating system, returns `None`. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// let key = Key::try_generate(); /// ``` pub fn try_generate() -> Option { use crate::secure::rand::RngCore; let mut rng = crate::secure::rand::thread_rng(); let mut key = Key::zero(); rng.try_fill_bytes(&mut key.0).ok()?; Some(key) } /// Returns the raw bytes of a key suitable for signing cookies. Guaranteed /// to be at least 32 bytes. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// let key = Key::generate(); /// let signing_key = key.signing(); /// ``` pub fn signing(&self) -> &[u8] { &self.0[..SIGNING_KEY_LEN] } /// Returns the raw bytes of a key suitable for encrypting cookies. /// Guaranteed to be at least 32 bytes. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// let key = Key::generate(); /// let encryption_key = key.encryption(); /// ``` pub fn encryption(&self) -> &[u8] { &self.0[SIGNING_KEY_LEN..] } /// Returns the raw bytes of the master key. Guaranteed to be at least 64 /// bytes. /// /// # Example /// /// ```rust /// use cookie::Key; /// /// let key = Key::generate(); /// let master_key = key.master(); /// ``` pub fn master(&self) -> &[u8] { &self.0 } } /// An error indicating an issue with generating or constructing a key. #[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] #[derive(Debug)] #[non_exhaustive] pub enum KeyError { /// Too few bytes (`.0`) were provided to generate a key. /// /// See [`Key::from()`] for minimum requirements. TooShort(usize), } impl std::error::Error for KeyError { } impl std::fmt::Display for KeyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { KeyError::TooShort(n) => { write!(f, "key material is too short: expected >= {} bytes, got {} bytes", COMBINED_KEY_LENGTH, n) } } } } impl TryFrom<&[u8]> for Key { type Error = KeyError; /// A fallible version of [`Key::from()`]. /// /// Succeeds when [`Key::from()`] succeds and returns an error where /// [`Key::from()`] panics, namely, if `key` is too short. /// /// # Example /// /// ```rust /// # use std::convert::TryFrom; /// use cookie::Key; /// /// # /* /// let key = { /* a cryptographically random key >= 64 bytes */ }; /// # */ /// # let key: &Vec = &(0..64).collect(); /// # let key: &[u8] = &key[..]; /// assert!(Key::try_from(key).is_ok()); /// /// // A key that's far too short to use. /// let key = &[1, 2, 3, 4][..]; /// assert!(Key::try_from(key).is_err()); /// ``` fn try_from(key: &[u8]) -> Result { if key.len() < COMBINED_KEY_LENGTH { Err(KeyError::TooShort(key.len())) } else { let mut output = Key::zero(); output.0.copy_from_slice(&key[..COMBINED_KEY_LENGTH]); Ok(output) } } } #[cfg(test)] mod test { use super::Key; #[test] fn from_works() { let key = Key::from(&(0..64).collect::>()); let signing: Vec = (0..32).collect(); assert_eq!(key.signing(), &*signing); let encryption: Vec = (32..64).collect(); assert_eq!(key.encryption(), &*encryption); } #[test] fn try_from_works() { use core::convert::TryInto; let data = (0..64).collect::>(); let key_res: Result = data[0..63].try_into(); assert!(key_res.is_err()); let key_res: Result = data.as_slice().try_into(); assert!(key_res.is_ok()); } #[test] #[cfg(feature = "key-expansion")] fn deterministic_derive() { let master_key: Vec = (0..32).collect(); let key_a = Key::derive_from(&master_key); let key_b = Key::derive_from(&master_key); assert_eq!(key_a.signing(), key_b.signing()); assert_eq!(key_a.encryption(), key_b.encryption()); assert_ne!(key_a.encryption(), key_a.signing()); let master_key_2: Vec = (32..64).collect(); let key_2 = Key::derive_from(&master_key_2); assert_ne!(key_2.signing(), key_a.signing()); assert_ne!(key_2.encryption(), key_a.encryption()); } #[test] fn non_deterministic_generate() { let key_a = Key::generate(); let key_b = Key::generate(); assert_ne!(key_a.signing(), key_b.signing()); assert_ne!(key_a.encryption(), key_b.encryption()); } #[test] fn debug_does_not_leak_key() { let key = Key::generate(); assert_eq!(format!("{:?}", key), "Key"); } } cookie-0.18.0/src/secure/macros.rs000064400000000000000000000032201046102023000150670ustar 00000000000000#[cfg(test)] macro_rules! assert_simple_behaviour { ($clear:expr, $secure:expr) => ({ assert_eq!($clear.iter().count(), 0); $secure.add(Cookie::new("name", "val")); assert_eq!($clear.iter().count(), 1); assert_eq!($secure.get("name").unwrap().value(), "val"); assert_ne!($clear.get("name").unwrap().value(), "val"); $secure.add(Cookie::new("another", "two")); assert_eq!($clear.iter().count(), 2); $clear.remove("another"); assert_eq!($clear.iter().count(), 1); $secure.remove("name"); assert_eq!($clear.iter().count(), 0); }) } #[cfg(test)] macro_rules! assert_secure_behaviour { ($clear:expr, $secure:expr) => ({ $secure.add(Cookie::new("secure", "secure")); assert!($clear.get("secure").unwrap().value() != "secure"); assert!($secure.get("secure").unwrap().value() == "secure"); let mut cookie = $clear.get("secure").unwrap().clone(); let new_val = format!("{}l", cookie.value()); cookie.set_value(new_val); $clear.add(cookie); assert!($secure.get("secure").is_none()); let mut cookie = $clear.get("secure").unwrap().clone(); cookie.set_value("foobar"); $clear.add(cookie); assert!($secure.get("secure").is_none()); }) } // This is courtesty of `static_assertions`. That library is Copyright (c) 2017 // Nikolai Vazquez. See https://github.com/nvzqz/static-assertions-rs for more. macro_rules! const_assert { ($x:expr $(,)?) => { #[allow(unknown_lints, clippy::eq_op)] const _: [(); 0 - !{ const ASSERT: bool = $x; ASSERT } as usize] = []; }; } cookie-0.18.0/src/secure/mod.rs000064400000000000000000000013001046102023000143570ustar 00000000000000extern crate rand; mod base64 { use base64::{DecodeError, Engine, prelude::BASE64_STANDARD}; /// Encode `input` as the standard base64 with padding. pub(crate) fn encode>(input: T) -> String { BASE64_STANDARD.encode(input) } /// Decode `input` as the standard base64 with padding. pub(crate) fn decode>(input: T) -> Result, DecodeError> { BASE64_STANDARD.decode(input) } } #[macro_use] mod macros; mod key; pub use self::key::*; #[cfg(feature = "private")] mod private; #[cfg(feature = "private")] pub use self::private::*; #[cfg(feature = "signed")] mod signed; #[cfg(feature = "signed")] pub use self::signed::*; cookie-0.18.0/src/secure/private.rs000064400000000000000000000241501046102023000152620ustar 00000000000000extern crate aes_gcm; use std::convert::TryInto; use std::borrow::{Borrow, BorrowMut}; use crate::secure::{base64, rand, Key}; use crate::{Cookie, CookieJar}; use self::aes_gcm::aead::{generic_array::GenericArray, Aead, AeadInPlace, KeyInit, Payload}; use self::aes_gcm::Aes256Gcm; use self::rand::RngCore; // Keep these in sync, and keep the key len synced with the `private` docs as // well as the `KEYS_INFO` const in secure::Key. pub(crate) const NONCE_LEN: usize = 12; pub(crate) const TAG_LEN: usize = 16; pub(crate) const KEY_LEN: usize = 32; /// A child cookie jar that provides authenticated encryption for its cookies. /// /// A _private_ child jar signs and encrypts all the cookies added to it and /// verifies and decrypts cookies retrieved from it. Any cookies stored in a /// `PrivateJar` are simultaneously assured confidentiality, integrity, and /// authenticity. In other words, clients cannot discover nor tamper with the /// contents of a cookie, nor can they fabricate cookie data. #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] pub struct PrivateJar { parent: J, key: [u8; KEY_LEN] } impl PrivateJar { /// Creates a new child `PrivateJar` with parent `parent` and key `key`. /// This method is typically called indirectly via the `signed` method of /// `CookieJar`. pub(crate) fn new(parent: J, key: &Key) -> PrivateJar { PrivateJar { parent, key: key.encryption().try_into().expect("enc key len") } } /// Encrypts the cookie's value with authenticated encryption providing /// confidentiality, integrity, and authenticity. fn encrypt_cookie(&self, cookie: &mut Cookie) { // Create a vec to hold the [nonce | cookie value | tag]. let cookie_val = cookie.value().as_bytes(); let mut data = vec![0; NONCE_LEN + cookie_val.len() + TAG_LEN]; // Split data into three: nonce, input/output, tag. Copy input. let (nonce, in_out) = data.split_at_mut(NONCE_LEN); let (in_out, tag) = in_out.split_at_mut(cookie_val.len()); in_out.copy_from_slice(cookie_val); // Fill nonce piece with random data. let mut rng = self::rand::thread_rng(); rng.try_fill_bytes(nonce).expect("couldn't random fill nonce"); let nonce = GenericArray::clone_from_slice(nonce); // Perform the actual sealing operation, using the cookie's name as // associated data to prevent value swapping. let aad = cookie.name().as_bytes(); let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); let aad_tag = aead.encrypt_in_place_detached(&nonce, aad, in_out) .expect("encryption failure!"); // Copy the tag into the tag piece. tag.copy_from_slice(&aad_tag); // Base64 encode [nonce | encrypted value | tag]. cookie.set_value(base64::encode(&data)); } /// Given a sealed value `str` and a key name `name`, where the nonce is /// prepended to the original value and then both are Base64 encoded, /// verifies and decrypts the sealed value and returns it. If there's a /// problem, returns an `Err` with a string describing the issue. fn unseal(&self, name: &str, value: &str) -> Result { let data = base64::decode(value).map_err(|_| "bad base64 value")?; if data.len() <= NONCE_LEN { return Err("length of decoded data is <= NONCE_LEN"); } let (nonce, cipher) = data.split_at(NONCE_LEN); let payload = Payload { msg: cipher, aad: name.as_bytes() }; let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); aead.decrypt(GenericArray::from_slice(nonce), payload) .map_err(|_| "invalid key/nonce/value: bad seal") .and_then(|s| String::from_utf8(s).map_err(|_| "bad unsealed utf8")) } /// Authenticates and decrypts `cookie`, returning the plaintext version if /// decryption succeeds or `None` otherwise. Authenticatation and decryption /// _always_ succeeds if `cookie` was generated by a `PrivateJar` with the /// same key as `self`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// assert!(jar.private(&key).get("name").is_none()); /// /// jar.private_mut(&key).add(Cookie::new("name", "value")); /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); /// /// let plain = jar.get("name").cloned().unwrap(); /// assert_ne!(plain.value(), "value"); /// let decrypted = jar.private(&key).decrypt(plain).unwrap(); /// assert_eq!(decrypted.value(), "value"); /// /// let plain = Cookie::new("plaintext", "hello"); /// assert!(jar.private(&key).decrypt(plain).is_none()); /// ``` pub fn decrypt(&self, mut cookie: Cookie<'static>) -> Option> { if let Ok(value) = self.unseal(cookie.name(), cookie.value()) { cookie.set_value(value); return Some(cookie); } None } } impl> PrivateJar { /// Returns a reference to the `Cookie` inside this jar with the name `name` /// and authenticates and decrypts the cookie's value, returning a `Cookie` /// with the decrypted value. If the cookie cannot be found, or the cookie /// fails to authenticate or decrypt, `None` is returned. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let jar = CookieJar::new(); /// assert!(jar.private(&key).get("name").is_none()); /// /// let mut jar = jar; /// let mut private_jar = jar.private_mut(&key); /// private_jar.add(Cookie::new("name", "value")); /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); /// ``` pub fn get(&self, name: &str) -> Option> { self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone())) } } impl> PrivateJar { /// Adds `cookie` to the parent jar. The cookie's value is encrypted with /// authenticated encryption assuring confidentiality, integrity, and /// authenticity. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.private_mut(&key).add(Cookie::new("name", "value")); /// /// assert_ne!(jar.get("name").unwrap().value(), "value"); /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); /// ``` pub fn add>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.encrypt_cookie(&mut cookie); self.parent.borrow_mut().add(cookie); } /// Adds an "original" `cookie` to parent jar. The cookie's value is /// encrypted with authenticated encryption assuring confidentiality, /// integrity, and authenticity. Adding an original cookie does not affect /// the [`CookieJar::delta()`] computation. This method is intended to be /// used to seed the cookie jar with cookies received from a client's HTTP /// message. /// /// For accurate `delta` computations, this method should not be called /// after calling `remove`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.private_mut(&key).add_original(Cookie::new("name", "value")); /// /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 0); /// ``` pub fn add_original>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.encrypt_cookie(&mut cookie); self.parent.borrow_mut().add_original(cookie); } /// Removes `cookie` from the parent jar. /// /// For correct removal, the passed in `cookie` must contain the same `path` /// and `domain` as the cookie that was initially set. /// /// This is identical to [`CookieJar::remove()`]. See the method's /// documentation for more details. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// let mut private_jar = jar.private_mut(&key); /// /// private_jar.add(("name", "value")); /// assert!(private_jar.get("name").is_some()); /// /// private_jar.remove("name"); /// assert!(private_jar.get("name").is_none()); /// ``` pub fn remove>>(&mut self, cookie: C) { self.parent.borrow_mut().remove(cookie); } } #[cfg(test)] mod test { use crate::{CookieJar, Cookie, Key}; #[test] fn simple() { let key = Key::generate(); let mut jar = CookieJar::new(); assert_simple_behaviour!(jar, jar.private_mut(&key)); } #[test] fn secure() { let key = Key::generate(); let mut jar = CookieJar::new(); assert_secure_behaviour!(jar, jar.private_mut(&key)); } #[test] fn roundtrip() { // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, 10, 180, 170, 187, 89, 252, 137, 110, 107]); let mut jar = CookieJar::new(); jar.add(Cookie::new("encrypted_with_ring014", "lObeZJorGVyeSWUA8khTO/8UCzFVBY9g0MGU6/J3NN1R5x11dn2JIA==")); jar.add(Cookie::new("encrypted_with_ring016", "SU1ujceILyMBg3fReqRmA9HUtAIoSPZceOM/CUpObROHEujXIjonkA==")); let private = jar.private(&key); assert_eq!(private.get("encrypted_with_ring014").unwrap().value(), "Tamper-proof"); assert_eq!(private.get("encrypted_with_ring016").unwrap().value(), "Tamper-proof"); } } cookie-0.18.0/src/secure/signed.rs000064400000000000000000000222631046102023000150640ustar 00000000000000use std::convert::TryInto; use std::borrow::{Borrow, BorrowMut}; use sha2::Sha256; use hmac::{Hmac, Mac}; use crate::secure::{base64, Key}; use crate::{Cookie, CookieJar}; // Keep these in sync, and keep the key len synced with the `signed` docs as // well as the `KEYS_INFO` const in secure::Key. pub(crate) const BASE64_DIGEST_LEN: usize = 44; pub(crate) const KEY_LEN: usize = 32; /// A child cookie jar that authenticates its cookies. /// /// A _signed_ child jar signs all the cookies added to it and verifies cookies /// retrieved from it. Any cookies stored in a `SignedJar` are provided /// integrity and authenticity. In other words, clients cannot tamper with the /// contents of a cookie nor can they fabricate cookie values, but the data is /// visible in plaintext. #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] pub struct SignedJar { parent: J, key: [u8; KEY_LEN], } impl SignedJar { /// Creates a new child `SignedJar` with parent `parent` and key `key`. This /// method is typically called indirectly via the `signed{_mut}` methods of /// `CookieJar`. pub(crate) fn new(parent: J, key: &Key) -> SignedJar { SignedJar { parent, key: key.signing().try_into().expect("sign key len") } } /// Signs the cookie's value providing integrity and authenticity. fn sign_cookie(&self, cookie: &mut Cookie) { // Compute HMAC-SHA256 of the cookie's value. let mut mac = Hmac::::new_from_slice(&self.key).expect("good key"); mac.update(cookie.value().as_bytes()); // Cookie's new value is [MAC | original-value]. let mut new_value = base64::encode(&mac.finalize().into_bytes()); new_value.push_str(cookie.value()); cookie.set_value(new_value); } /// Given a signed value `str` where the signature is prepended to `value`, /// verifies the signed value and returns it. If there's a problem, returns /// an `Err` with a string describing the issue. fn _verify(&self, cookie_value: &str) -> Result { if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) { return Err("missing or invalid digest"); } // Split [MAC | original-value] into its two parts. let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; // Perform the verification. let mut mac = Hmac::::new_from_slice(&self.key).expect("good key"); mac.update(value.as_bytes()); mac.verify_slice(&digest) .map(|_| value.to_string()) .map_err(|_| "value did not verify") } /// Verifies the authenticity and integrity of `cookie`, returning the /// plaintext version if verification succeeds or `None` otherwise. /// Verification _always_ succeeds if `cookie` was generated by a /// `SignedJar` with the same key as `self`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// assert!(jar.signed(&key).get("name").is_none()); /// /// jar.signed_mut(&key).add(("name", "value")); /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); /// /// let plain = jar.get("name").cloned().unwrap(); /// assert_ne!(plain.value(), "value"); /// let verified = jar.signed(&key).verify(plain).unwrap(); /// assert_eq!(verified.value(), "value"); /// /// let plain = Cookie::new("plaintext", "hello"); /// assert!(jar.signed(&key).verify(plain).is_none()); /// ``` pub fn verify(&self, mut cookie: Cookie<'static>) -> Option> { if let Ok(value) = self._verify(cookie.value()) { cookie.set_value(value); return Some(cookie); } None } } impl> SignedJar { /// Returns a reference to the `Cookie` inside this jar with the name `name` /// and verifies the authenticity and integrity of the cookie's value, /// returning a `Cookie` with the authenticated value. If the cookie cannot /// be found, or the cookie fails to verify, `None` is returned. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let jar = CookieJar::new(); /// assert!(jar.signed(&key).get("name").is_none()); /// /// let mut jar = jar; /// let mut signed_jar = jar.signed_mut(&key); /// signed_jar.add(Cookie::new("name", "value")); /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); /// ``` pub fn get(&self, name: &str) -> Option> { self.parent.borrow().get(name).and_then(|c| self.verify(c.clone())) } } impl> SignedJar { /// Adds `cookie` to the parent jar. The cookie's value is signed assuring /// integrity and authenticity. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.signed_mut(&key).add(("name", "value")); /// /// assert_ne!(jar.get("name").unwrap().value(), "value"); /// assert!(jar.get("name").unwrap().value().contains("value")); /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); /// ``` pub fn add>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.sign_cookie(&mut cookie); self.parent.borrow_mut().add(cookie); } /// Adds an "original" `cookie` to this jar. The cookie's value is signed /// assuring integrity and authenticity. Adding an original cookie does not /// affect the [`CookieJar::delta()`] computation. This method is intended /// to be used to seed the cookie jar with cookies received from a client's /// HTTP message. /// /// For accurate `delta` computations, this method should not be called /// after calling `remove`. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.signed_mut(&key).add_original(("name", "value")); /// /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 0); /// ``` pub fn add_original>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.sign_cookie(&mut cookie); self.parent.borrow_mut().add_original(cookie); } /// Removes `cookie` from the parent jar. /// /// For correct removal, the passed in `cookie` must contain the same `path` /// and `domain` as the cookie that was initially set. /// /// This is identical to [`CookieJar::remove()`]. See the method's /// documentation for more details. /// /// # Example /// /// ```rust /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// let mut signed_jar = jar.signed_mut(&key); /// /// signed_jar.add(("name", "value")); /// assert!(signed_jar.get("name").is_some()); /// /// signed_jar.remove("name"); /// assert!(signed_jar.get("name").is_none()); /// ``` pub fn remove>>(&mut self, cookie: C) { self.parent.borrow_mut().remove(cookie.into()); } } #[cfg(test)] mod test { use crate::{CookieJar, Cookie, Key}; #[test] fn simple() { let key = Key::generate(); let mut jar = CookieJar::new(); assert_simple_behaviour!(jar, jar.signed_mut(&key)); } #[test] fn private() { let key = Key::generate(); let mut jar = CookieJar::new(); assert_secure_behaviour!(jar, jar.signed_mut(&key)); } #[test] fn roundtrip() { // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, 10, 180, 170, 187, 89, 252, 137, 110, 107]); let mut jar = CookieJar::new(); jar.add(Cookie::new("signed_with_ring014", "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); jar.add(Cookie::new("signed_with_ring016", "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); let signed = jar.signed(&key); assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof"); assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof"); } #[test] fn issue_178() { let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£"; let c = Cookie::parse(data).expect("failed to parse cookie"); let key = Key::from(&[0u8; 64]); let mut jar = CookieJar::new(); let signed = jar.signed_mut(&key); assert!(signed.verify(c).is_none()); } }