diff options
Diffstat (limited to 'third_party/rust/cookie')
20 files changed, 4685 insertions, 0 deletions
diff --git a/third_party/rust/cookie/.cargo-checksum.json b/third_party/rust/cookie/.cargo-checksum.json new file mode 100644 index 0000000000..6323f44cf7 --- /dev/null +++ b/third_party/rust/cookie/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"0b902430f29f7baa50700990eed90c82cb4b90271a11774189ac90afd81fa6fc","Cargo.toml":"f98745c6f3516b019ce7c6f142106e4c58e85beefc03fb1bb934ab6f916a9507","LICENSE-APACHE":"2773e20df8f4c52a026b5b578c7f2457341f5aa3fb6612fd87e1d2e1bd8f48ad","LICENSE-MIT":"f28420c1906af38be726cd7842798f0194b27c41d6051c0d1e9ee348b8a4a0ea","README.md":"23fb31e855c9504d315be851b3dcfd4b22d8a4ad28e24c1d99d7ecb4d60c3d9d","build.rs":"75c45e6b8566ca721dd5759b6ef16e365d5cb201660eee5ec04e278f9d1eefe2","scripts/test.sh":"a76191d56d96c32efcb6883e0983e86beb4c6842e6e5c5a8bfded4c8183ff6f6","src/builder.rs":"238884aebf7fa2e4ab940faee4a5a2f5d6add35d2214ac644c36ec0ec64d7829","src/delta.rs":"4232e3acf98a70b111329c92cd823ba0419d4a12c3894091a318ae823dfdb225","src/draft.rs":"4f39d7acf3d3e868c013bab9f97ac570b983eed94d64fd59dba39343b870e4e0","src/expiration.rs":"188be15a5dd20d3471daa483609377ab66d876eb77afad1fc44891c18f251efd","src/jar.rs":"e91ba170610cc5b73a5cce8de1f6d960b26303ee5dff49822728e86a5fc64218","src/lib.rs":"b2355763311361cb681de12db9e0a0ac41a0144d89fdacb9b6ab27d28a403787","src/parse.rs":"b37bd10631edc373ac82d9c9e5968416b04b3b705630ea4d4f9b1d7db9e695f6","src/secure/key.rs":"47300e6c22b5debb6a7aeabe685449059d38f505d90f66ca4e553e866af49959","src/secure/macros.rs":"18377b3fffdb2a1ad754f98f65a69d40a31fb69185fed231a4808ed4319985e4","src/secure/mod.rs":"6b2cf8486244545d51ecc5bd880845ae3f8d681ed86592e93cc4706b34704dcc","src/secure/private.rs":"459fe66fb465fd3d17eb6a3308a162d2f3ec0db90644fedee4265645f621f992","src/secure/signed.rs":"2375c29ca816e093fbee1db5631630f0ea47d92b1d16db458b7be14f30765117"},"package":"e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"}
\ No newline at end of file diff --git a/third_party/rust/cookie/CHANGELOG.md b/third_party/rust/cookie/CHANGELOG.md new file mode 100644 index 0000000000..8416fc29e6 --- /dev/null +++ b/third_party/rust/cookie/CHANGELOG.md @@ -0,0 +1,155 @@ +# 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<Expiration>` 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. diff --git a/third_party/rust/cookie/Cargo.toml b/third_party/rust/cookie/Cargo.toml new file mode 100644 index 0000000000..a6db2d1d1b --- /dev/null +++ b/third_party/rust/cookie/Cargo.toml @@ -0,0 +1,101 @@ +# 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.16.2" +authors = [ + "Sergio Benitez <sb@sergio.bz>", + "Alex Crichton <alex@alexcrichton.com>", +] +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.20" +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", +] diff --git a/third_party/rust/cookie/LICENSE-APACHE b/third_party/rust/cookie/LICENSE-APACHE new file mode 100644 index 0000000000..5cd49dc7e9 --- /dev/null +++ b/third_party/rust/cookie/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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. diff --git a/third_party/rust/cookie/LICENSE-MIT b/third_party/rust/cookie/LICENSE-MIT new file mode 100644 index 0000000000..e832801a43 --- /dev/null +++ b/third_party/rust/cookie/LICENSE-MIT @@ -0,0 +1,26 @@ +Copyright (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. diff --git a/third_party/rust/cookie/README.md b/third_party/rust/cookie/README.md new file mode 100644 index 0000000000..073db29e80 --- /dev/null +++ b/third_party/rust/cookie/README.md @@ -0,0 +1,41 @@ +# 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.16" +``` + +See the [documentation](http://docs.rs/cookie) for detailed usage information. + +# MSRV + +The minimum supported `rustc` version for cookie `0.16` is `1.53`. + +The minimum supported `rustc` version for cookie `0.15` is `1.41`. + +# License + +This project is licensed under either of + + * 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) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in `cookie-rs` by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/third_party/rust/cookie/build.rs b/third_party/rust/cookie/build.rs new file mode 100644 index 0000000000..66f1ea2136 --- /dev/null +++ b/third_party/rust/cookie/build.rs @@ -0,0 +1,5 @@ +fn main() { + if let Some(true) = version_check::supports_feature("doc_cfg") { + println!("cargo:rustc-cfg=nightly"); + } +} diff --git a/third_party/rust/cookie/scripts/test.sh b/third_party/rust/cookie/scripts/test.sh new file mode 100755 index 0000000000..894d468eb7 --- /dev/null +++ b/third_party/rust/cookie/scripts/test.sh @@ -0,0 +1,19 @@ +#!/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 diff --git a/third_party/rust/cookie/src/builder.rs b/third_party/rust/cookie/src/builder.rs new file mode 100644 index 0000000000..78d1219a9a --- /dev/null +++ b/third_party/rust/cookie/src/builder.rs @@ -0,0 +1,246 @@ +use std::borrow::Cow; + +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. +/// 3. Call [`CookieBuilder::finish()`] to retrieve the built cookie. +/// +/// # Example +/// +/// ```rust +/// # extern crate cookie; +/// use cookie::Cookie; +/// use cookie::time::Duration; +/// +/// # fn main() { +/// let cookie: Cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .max_age(Duration::days(1)) +/// .finish(); +/// # } +/// ``` +#[derive(Debug, Clone)] +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; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn new<N, V>(name: N, value: V) -> Self + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + CookieBuilder { cookie: Cookie::new(name, value) } + } + + /// Sets the `expires` field in the cookie being built. + /// + /// # 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()) + /// .finish(); + /// + /// assert!(c.expires().is_some()); + /// + /// let c = Cookie::build("foo", "bar") + /// .expires(None) + /// .finish(); + /// + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// # } + /// ``` + #[inline] + pub fn expires<E: Into<Expiration>>(mut self, when: E) -> Self { + self.cookie.set_expires(when); + self + } + + /// Sets the `max_age` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .max_age(Duration::minutes(30)) + /// .finish(); + /// + /// assert_eq!(c.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") + /// .finish(); + /// + /// assert_eq!(c.domain(), Some("www.rust-lang.org")); + /// ``` + pub fn domain<D: Into<Cow<'c, str>>>(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("/") + /// .finish(); + /// + /// assert_eq!(c.path(), Some("/")); + /// ``` + pub fn path<P: Into<Cow<'c, str>>>(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) + /// .finish(); + /// + /// assert_eq!(c.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) + /// .finish(); + /// + /// assert_eq!(c.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) + /// .finish(); + /// + /// assert_eq!(c.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() + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// # assert!(c.expires().is_some()); + /// # } + /// ``` + #[inline] + pub fn permanent(mut self) -> Self { + self.cookie.make_permanent(); + self + } + + /// Finishes building and returns the built `Cookie`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .domain("crates.io") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// assert_eq!(c.domain(), Some("crates.io")); + /// assert_eq!(c.path(), Some("/")); + /// ``` + #[inline] + pub fn finish(self) -> Cookie<'c> { + self.cookie + } +} diff --git a/third_party/rust/cookie/src/delta.rs b/third_party/rust/cookie/src/delta.rs new file mode 100644 index 0000000000..3ef35fadd7 --- /dev/null +++ b/third_party/rust/cookie/src/delta.rs @@ -0,0 +1,65 @@ +use 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 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<H: Hasher>(&self, state: &mut H) { + self.name().hash(state); + } +} + +impl Borrow<str> for DeltaCookie { + fn borrow(&self) -> &str { + self.name() + } +} diff --git a/third_party/rust/cookie/src/draft.rs b/third_party/rust/cookie/src/draft.rs new file mode 100644 index 0000000000..e625e81c90 --- /dev/null +++ b/third_party/rust/cookie/src/draft.rs @@ -0,0 +1,108 @@ +//! 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"), + } + } +} diff --git a/third_party/rust/cookie/src/expiration.rs b/third_party/rust/cookie/src/expiration.rs new file mode 100644 index 0000000000..2ff2885483 --- /dev/null +++ b/third_party/rust/cookie/src/expiration.rs @@ -0,0 +1,137 @@ +use time::OffsetDateTime; + +/// A cookie's expiration: either session or a date-time. +/// +/// An `Expiration` is constructible via `Expiration::from()` with an +/// `Option<OffsetDateTime>` or an `OffsetDateTime`: +/// +/// * `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<OffsetDateTime> { + 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<F>(self, f: F) -> Self + where F: FnOnce(OffsetDateTime) -> OffsetDateTime + { + match self { + Expiration::Session => Expiration::Session, + Expiration::DateTime(v) => Expiration::DateTime(f(v)), + } + } +} + +impl<T: Into<Option<OffsetDateTime>>> From<T> for Expiration { + fn from(option: T) -> Self { + match option.into() { + Some(value) => Expiration::DateTime(value), + None => Expiration::Session + } + } +} diff --git a/third_party/rust/cookie/src/jar.rs b/third_party/rust/cookie/src/jar.rs new file mode 100644 index 0000000000..83f2a15fdc --- /dev/null +++ b/third_party/rust/cookie/src/jar.rs @@ -0,0 +1,728 @@ +use 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::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 [new](#method.new) and calls to +/// [`add_original`](#method.add_original): +/// +/// ```rust +/// use cookie::{Cookie, CookieJar}; +/// +/// let mut jar = CookieJar::new(); +/// jar.add_original(Cookie::new("name", "value")); +/// jar.add_original(Cookie::new("second", "another")); +/// ``` +/// +/// Cookies can be added via [add](#method.add) and removed via +/// [remove](#method.remove). Finally, cookies can be looked up via +/// [get](#method.get): +/// +/// ```rust +/// # use cookie::{Cookie, CookieJar}; +/// let mut jar = CookieJar::new(); +/// jar.add(Cookie::new("a", "one")); +/// jar.add(Cookie::new("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(Cookie::named("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(Cookie::new("original", "value")); +/// assert_eq!(jar.delta().count(), 0); +/// +/// // new cookies result in an equivalent `Cookie` in the delta +/// jar.add(Cookie::new("a", "one")); +/// jar.add(Cookie::new("b", "two")); +/// assert_eq!(jar.delta().count(), 2); +/// +/// // removing an original cookie adds a "removal" cookie to the delta +/// jar.remove(Cookie::named("original")); +/// assert_eq!(jar.delta().count(), 3); +/// +/// // removing a new cookie that was added removes that `Cookie` from the delta +/// jar.remove(Cookie::named("a")); +/// assert_eq!(jar.delta().count(), 2); +/// ``` +#[derive(Default, Debug, Clone)] +pub struct CookieJar { + original_cookies: HashSet<DeltaCookie>, + delta_cookies: HashSet<DeltaCookie>, +} + +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(Cookie::new("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(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("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: Cookie<'static>) { + self.original_cookies.replace(DeltaCookie::added(cookie)); + } + + /// 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(Cookie::new("name", "value")); + /// jar.add(Cookie::new("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: Cookie<'static>) { + self.delta_cookies.replace(DeltaCookie::added(cookie)); + } + + /// 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 + /// # extern crate cookie; + /// use cookie::{CookieJar, Cookie}; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let mut jar = CookieJar::new(); + /// + /// // Assume this cookie originally had a path of "/" and domain of "a.b". + /// jar.add_original(Cookie::new("name", "value")); + /// + /// // If the path and domain were set, they must be provided to `remove`. + /// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish()); + /// + /// // 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(Cookie::new("name", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// + /// jar.remove(Cookie::named("name")); + /// assert_eq!(jar.delta().count(), 0); + /// + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add(Cookie::new("name", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// + /// jar.remove(Cookie::named("name")); + /// assert_eq!(jar.delta().count(), 1); + /// ``` + pub fn remove(&mut self, mut cookie: Cookie<'static>) { + 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. 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(Cookie::new("name", "value")); + /// jar.add(Cookie::new("key", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// assert_eq!(jar.iter().count(), 2); + /// + /// // Now force remove the original cookie. + /// jar.force_remove(&Cookie::named("name")); + /// assert_eq!(jar.delta().count(), 1); + /// assert_eq!(jar.iter().count(), 1); + /// + /// // Now force remove the new cookie. + /// jar.force_remove(&Cookie::named("key")); + /// assert_eq!(jar.delta().count(), 0); + /// assert_eq!(jar.iter().count(), 0); + /// # } + /// ``` + pub fn force_remove<'a>(&mut self, cookie: &Cookie<'a>) { + self.original_cookies.remove(cookie.name()); + self.delta_cookies.remove(cookie.name()); + } + + /// 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(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("language", "Rust")); + /// + /// // These operations, represented by delta cookies, will be reset. + /// jar.add(Cookie::new("language", "C++")); + /// jar.remove(Cookie::named("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(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("second", "two")); + /// + /// // Add new cookies. + /// jar.add(Cookie::new("new", "third")); + /// jar.add(Cookie::new("another", "fourth")); + /// jar.add(Cookie::new("yac", "fifth")); + /// + /// // Remove some cookies. + /// jar.remove(Cookie::named("name")); + /// jar.remove(Cookie::named("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(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("second", "two")); + /// + /// jar.add(Cookie::new("new", "third")); + /// jar.add(Cookie::new("another", "fourth")); + /// jar.add(Cookie::new("yac", "fifth")); + /// + /// jar.remove(Cookie::named("name")); + /// jar.remove(Cookie::named("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(Cookie::new("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(Cookie::new("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(Cookie::new("private", "text")); + /// + /// // Remove a cookie using the child jar. + /// jar.private_mut(&key).remove(Cookie::named("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(Cookie::new("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(Cookie::new("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::{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(Cookie::new("signed", "text")); + /// + /// // Remove a cookie. + /// jar.signed_mut(&key).remove(Cookie::named("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) + } +} + +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<HashSetIter<'a, DeltaCookie>, 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(Cookie::new("test", "")); + c.add(Cookie::new("test2", "")); + c.remove(Cookie::named("test")); + + assert!(c.get("test").is_none()); + assert!(c.get("test2").is_some()); + + c.add(Cookie::new("test3", "")); + c.remove(Cookie::named("test2")); + c.remove(Cookie::named("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: 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(Cookie::named("test")); + assert_eq!(c.iter().count(), 5); + + c.remove(Cookie::named("signed")); + c.remove(Cookie::named("test2")); + assert_eq!(c.iter().count(), 3); + + c.add(Cookie::new("test2", "test2")); + assert_eq!(c.iter().count(), 4); + + c.remove(Cookie::named("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(Cookie::named("test")); + c.remove(Cookie::named("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(Cookie::named("name")); + assert_eq!(jar.delta().count(), 0); + + jar.add_original(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 0); + + jar.remove(Cookie::named("name")); + assert_eq!(jar.delta().count(), 1); + + jar.add(Cookie::new("name", "val")); + assert_eq!(jar.delta().count(), 1); + + jar.remove(Cookie::named("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(Cookie::named("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(Cookie::named("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(Cookie::named("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(Cookie::named("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(Cookie::build("name", "val").finish()); + 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("/").finish()); + 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); + } +} diff --git a/third_party/rust/cookie/src/lib.rs b/third_party/rust/cookie/src/lib.rs new file mode 100644 index 0000000000..392728efbc --- /dev/null +++ b/third_party/rust/cookie/src/lib.rs @@ -0,0 +1,1422 @@ +//! 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.16" +//! ``` +//! +//! # 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))] + +#![doc(html_root_url = "https://docs.rs/cookie/0.16")] +#![deny(missing_docs)] + +pub use time; + +mod builder; +mod parse; +mod jar; +mod delta; +mod draft; +mod expiration; + +#[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::draft::*; +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> { + /// Retrieves the string `self` corresponds to. If `self` is derived from + /// indexes, 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<str>>) -> &'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"); +/// ``` +/// +/// To construct more elaborate cookies, use [`Cookie::build()`] and +/// [`CookieBuilder`] methods: +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .finish(); +/// ``` +#[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<Cow<'c, str>>, + /// The cookie's name. + name: CookieStr<'c>, + /// The cookie's value. + value: CookieStr<'c>, + /// The cookie's expiration, if any. + expires: Option<Expiration>, + /// The cookie's maximum age, if any. + max_age: Option<Duration>, + /// The cookie's domain, if any. + domain: Option<CookieStr<'c>>, + /// The cookie's path domain, if any. + path: Option<CookieStr<'c>>, + /// Whether this cookie was marked Secure. + secure: Option<bool>, + /// Whether this cookie was marked HttpOnly. + http_only: Option<bool>, + /// The draft `SameSite` attribute. + same_site: Option<SameSite>, +} + +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")); + /// ``` + pub fn new<N, V>(name: N, value: V) -> Self + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + 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()); + /// ``` + pub fn named<N>(name: N) -> Cookie<'c> + where N: Into<Cow<'c, str>> + { + Cookie::new(name, "") + } + + /// Creates a new `CookieBuilder` instance from the given key and value + /// strings. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn build<N, V>(name: N, value: V) -> CookieBuilder<'c> + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + CookieBuilder::new(name, value) + } + + /// 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)); + /// ``` + pub fn parse<S>(s: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + parse_cookie(s, 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)); + /// ``` + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + pub fn parse_encoded<S>(s: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + parse_cookie(s, 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`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// 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 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 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<bool> { + 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<bool> { + 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<SameSite> { + 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<Duration> { + 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. + /// + /// # 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")); + /// ``` + #[inline] + pub fn domain(&self) -> Option<&str> { + match self.domain { + Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), + 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).finish(); + /// 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<Expiration> { + 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).finish(); + /// 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<OffsetDateTime> { + 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<N: Into<Cow<'c, str>>>(&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<V: Into<Cow<'c, str>>>(&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<T: Into<Option<bool>>>(&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<T: Into<Option<bool>>>(&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<T: Into<Option<SameSite>>>(&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<D: Into<Option<Duration>>>(&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<P: Into<Cow<'c, str>>>(&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<D: Into<Cow<'c, str>>>(&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<T: Into<Expiration>>(&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`. + /// + /// 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)) => domain.to_raw_str(string), + _ => 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).finish(); + /// 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("/").finish(); + /// 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) + } +} + +#[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).finish(); +/// 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("/") + /// .finish(); + /// + /// 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<Cookie<'static>, ParseError> { + Cookie::parse(s).map(|c| c.into_owned()) + } +} + +impl<'a, 'b> PartialEq<Cookie<'b>> 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 + } +} + +#[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).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); + + let cookie = Cookie::build("foo", "bar") + .max_age(Duration::seconds(10)).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); + + let cookie = Cookie::build("foo", "bar") + .secure(true).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Secure"); + + let cookie = Cookie::build("foo", "bar") + .path("/").finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); + + let cookie = Cookie::build("foo", "bar") + .domain("www.rust-lang.org").finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.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).finish(); + 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).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); + + let cookie = Cookie::build("foo", "bar") + .same_site(SameSite::Lax).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); + + let mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); + + cookie.set_same_site(None); + assert_eq!(&cookie.to_string(), "foo=bar"); + + let mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .secure(false) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); + cookie.set_secure(true); + assert_eq!(&cookie.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).finish(); + 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).finish(); + 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::build("foo !%?=", "bar;;, a").finish(); + 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")); + } +} diff --git a/third_party/rust/cookie/src/parse.rs b/third_party/rust/cookie/src/parse.rs new file mode 100644 index 0000000000..dcd15b7b32 --- /dev/null +++ b/third_party/rust/cookie/src/parse.rs @@ -0,0 +1,550 @@ +use 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<Utf8Error> for ParseError { + fn from(error: Utf8Error) -> ParseError { + ParseError::Utf8Error(error) + } +} + +impl Error for ParseError { + fn description(&self) -> &str { + self.as_str() + } +} + +fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> { + 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((start, end)) +} + +#[cfg(feature = "percent-encode")] +fn name_val_decoded( + name: &str, + val: &str +) -> Result<Option<(CookieStr<'static>, 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<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> { + unreachable!("This function should never be called with 'percent-encode' disabled!") +} + +fn trim_quotes(s: &str) -> &str { + if s.len() < 2 { + return s; + } + + match (s.chars().next(), s.chars().last()) { + (Some('"'), Some('"')) => &s[1..(s.len() - 1)], + _ => s + } +} + +// 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<Cookie<'c>, 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) => { + let (key, value) = (key_value[..i].trim(), key_value[(i + 1)..].trim()); + (key, trim_quotes(value).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_indexes = indexes_of(name, s).expect("name sub"); + let value_indexes = indexes_of(value, s).expect("value sub"); + let name = CookieStr::Indexed(name_indexes.0, name_indexes.1); + let value = CookieStr::Indexed(value_indexes.0, value_indexes.1); + (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::<i64>() + .map(Duration::seconds) + .unwrap_or_else(|_| Duration::seconds(i64::max_value()))) + } + }, + ("domain", Some(mut domain)) if !domain.is_empty() => { + if domain.starts_with('.') { + domain = &domain[1..]; + } + + let (i, j) = indexes_of(domain, s).expect("domain sub"); + cookie.domain = Some(CookieStr::Indexed(i, j)); + } + ("path", Some(v)) => { + let (i, j) = indexes_of(v, s).expect("path sub"); + cookie.path = Some(CookieStr::Indexed(i, j)); + } + ("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<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> +{ + 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<OffsetDateTime, time::Error> { + // 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) + .finish(); + + 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) + .finish(); + + 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) + .finish(); + + 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::build("foo", "bar=baz").finish(); + assert_eq_parse!("foo=bar=baz", expected); + + let expected = Cookie::build("foo", "\"bar\"").finish(); + assert_eq_parse!("foo=\"\"bar\"\"", expected); + + let expected = Cookie::build("foo", "\"bar").finish(); + assert_eq_parse!("foo= \"bar", expected); + assert_eq_parse!("foo=\"bar ", expected); + assert_eq_parse!("foo=\"\"bar\"", expected); + assert_eq_parse!("foo=\"\"bar \"", expected); + assert_eq_parse!("foo=\"\"bar \" ", expected); + + let expected = Cookie::build("foo", "bar\"").finish(); + assert_eq_parse!("foo=bar\"", expected); + assert_eq_parse!("foo=\"bar\"\"", expected); + assert_eq_parse!("foo=\" bar\"\"", expected); + assert_eq_parse!("foo=\" bar\" \" ", expected); + + let mut expected = Cookie::build("foo", "bar").finish(); + assert_eq_parse!("foo=bar", expected); + assert_eq_parse!("foo = bar", expected); + 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); + + let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish(); + 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); + + 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())) + .finish(); + + 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)) + .finish(); + let too_many_seconds = (max_seconds as u64) + 1; + assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected); + } +} diff --git a/third_party/rust/cookie/src/secure/key.rs b/third_party/rust/cookie/src/secure/key.rs new file mode 100644 index 0000000000..9c2228eb3a --- /dev/null +++ b/third_party/rust/cookie/src/secure/key.rs @@ -0,0 +1,301 @@ +use 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 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<u8> = &(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<u8> = &(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::<sha2::Sha256>::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<Key> { + 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<u8> = &(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<Self, Self::Error> { + 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::<Vec<_>>()); + + let signing: Vec<u8> = (0..32).collect(); + assert_eq!(key.signing(), &*signing); + + let encryption: Vec<u8> = (32..64).collect(); + assert_eq!(key.encryption(), &*encryption); + } + + #[test] + fn try_from_works() { + use core::convert::TryInto; + let data = (0..64).collect::<Vec<_>>(); + let key_res: Result<Key, _> = data[0..63].try_into(); + assert!(key_res.is_err()); + + let key_res: Result<Key, _> = data.as_slice().try_into(); + assert!(key_res.is_ok()); + } + + #[test] + #[cfg(feature = "key-expansion")] + fn deterministic_derive() { + let master_key: Vec<u8> = (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<u8> = (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()); + } +} diff --git a/third_party/rust/cookie/src/secure/macros.rs b/third_party/rust/cookie/src/secure/macros.rs new file mode 100644 index 0000000000..dfbddcf928 --- /dev/null +++ b/third_party/rust/cookie/src/secure/macros.rs @@ -0,0 +1,49 @@ +#[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(Cookie::named("another")); + assert_eq!($clear.iter().count(), 1); + + $secure.remove(Cookie::named("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] = []; + }; +} diff --git a/third_party/rust/cookie/src/secure/mod.rs b/third_party/rust/cookie/src/secure/mod.rs new file mode 100644 index 0000000000..ca066888e7 --- /dev/null +++ b/third_party/rust/cookie/src/secure/mod.rs @@ -0,0 +1,14 @@ +extern crate rand; +extern crate base64; + +#[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::*; diff --git a/third_party/rust/cookie/src/secure/private.rs b/third_party/rust/cookie/src/secure/private.rs new file mode 100644 index 0000000000..1264c3983c --- /dev/null +++ b/third_party/rust/cookie/src/secure/private.rs @@ -0,0 +1,264 @@ +extern 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<J> { + parent: J, + key: [u8; KEY_LEN] +} + +impl<J> PrivateJar<J> { + /// 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<J> { + 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<String, &'static str> { + 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<Cookie<'static>> { + if let Ok(value) = self.unseal(cookie.name(), cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> PrivateJar<J> { + /// 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<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> PrivateJar<J> { + /// 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, mut cookie: Cookie<'static>) { + 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, mut cookie: Cookie<'static>) { + 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(Cookie::new("name", "value")); + /// assert!(private_jar.get("name").is_some()); + /// + /// private_jar.remove(Cookie::named("name")); + /// assert!(private_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + 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"); + } +} diff --git a/third_party/rust/cookie/src/secure/signed.rs b/third_party/rust/cookie/src/secure/signed.rs new file mode 100644 index 0000000000..b46fae4e80 --- /dev/null +++ b/third_party/rust/cookie/src/secure/signed.rs @@ -0,0 +1,251 @@ +use 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<J> { + parent: J, + key: [u8; KEY_LEN], +} + +impl<J> SignedJar<J> { + /// 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<J> { + 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::<Sha256>::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<String, &'static str> { + 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::<Sha256>::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(Cookie::new("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<Cookie<'static>> { + if let Ok(value) = self._verify(cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> SignedJar<J> { + /// 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<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.verify(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> SignedJar<J> { + /// 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(Cookie::new("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, mut cookie: Cookie<'static>) { + 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(Cookie::new("name", "value")); + /// + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original(&mut self, mut cookie: Cookie<'static>) { + 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(Cookie::new("name", "value")); + /// assert!(signed_jar.get("name").is_some()); + /// + /// signed_jar.remove(Cookie::named("name")); + /// assert!(signed_jar.get("name").is_none()); + /// ``` + pub fn remove(&mut self, cookie: Cookie<'static>) { + 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.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()); + } +} |