Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"58f6a54bde15e3d589b7a16f97679198e0115efd7ea307c53aa8a135a9e76f79",".github/workflows/ci.yml":"fc082274e11b6a986c14d341873daf94efa0d7abade06746c84f6606d2e2df68",".travis.yml":"66e5b922d11c625fc740219e9d1a60da33060cb2a79c56c4adcbef0655e20c76","CHANGELOG.md":"77d210f118790902c2f75d627421fef231724f522cd870826cc48ed48099c0ae","Cargo.toml":"5cf4dcffcb9cbab1c8cd9d2cf9f39ed8aa4fc5dbafc0146d9999b4082db9efbf","Cargo.toml.orig":"4167464391b754dd02996e1deb55a414ad9f897822894e484ba19a14cd48cf58","LICENSE-APACHE":"2773e20df8f4c52a026b5b578c7f2457341f5aa3fb6612fd87e1d2e1bd8f48ad","LICENSE-MIT":"f28420c1906af38be726cd7842798f0194b27c41d6051c0d1e9ee348b8a4a0ea","README.md":"07cb9b30d45eb5cb94d8bdbba9711755b89b2d7a7d847fda156462be71ed2003","build.rs":"75c45e6b8566ca721dd5759b6ef16e365d5cb201660eee5ec04e278f9d1eefe2","scripts/test.sh":"a76191d56d96c32efcb6883e0983e86beb4c6842e6e5c5a8bfded4c8183ff6f6","src/builder.rs":"dadac10c2f7baf039da437c19f6337cdb14714593eb62741412d64910a77439e","src/delta.rs":"e1d1c91f32adc004dfc5a72f065f6560c6d6941656ae0acd2040f7e784d0b42e","src/expiration.rs":"f6370ea7f552fb15f2ebca2d9caad3c9995aa73c225f35908f9675ad0381350c","src/jar.rs":"ca67cd1b2057d10ecb8ec1e3584b0759bed417cbc78b93334b114a6735862cdf","src/lib.rs":"1d9e363efe1f3150e919f34c5d03a04ff456b43fc62c71b0d49791be1c62c005","src/parse.rs":"a565e42964f13256a05b6a4e02a26962861294c0ab04e6ce564b5575d2d5c51a","src/prefix.rs":"2686f7aa32931808ebb4e449a71f3818cd8246e8d6f97a7f08ad75ebaae7c9ef","src/same_site.rs":"4f39d7acf3d3e868c013bab9f97ac570b983eed94d64fd59dba39343b870e4e0","src/secure/key.rs":"e80387dc9311070fbb3310cc6881fcea2dd3d5202c5ac07fbc7bd6e187870b82","src/secure/macros.rs":"61fad1d1e4913035b2a2de7e103ab9c3b4eb93cf7ab6d127ea2adbc278325303","src/secure/mod.rs":"18faab38446118dbdb3687602e61ccadb99ecae6f4697d55e0a559af7f73266d","src/secure/private.rs":"5d798a8bbc99bf7a9596f33b15c0b526f32da56ec38e9c9754574f2d2d6683b5","src/secure/signed.rs":"605b529e6e2a96853dcdf303d145ba365528a9ee7399db1042c4a237c383d6f7"},"package":"4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "ba46fc5e97a1271435f38509d109e125a473bc82"
},
"path_in_vcs": ""
}
+56
View File
@@ -0,0 +1,56 @@
name: CI
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
MSRV: "1.56"
jobs:
test:
name: "${{ matrix.os.name }} (${{ matrix.toolchain }})"
continue-on-error: false
runs-on: ${{ matrix.os.distro }}
strategy:
fail-fast: false
matrix:
os:
- { name: Linux, distro: ubuntu-latest }
- { name: Windows, distro: windows-latest }
toolchain: [stable, beta, nightly]
steps:
- name: Checkout Sources
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Run Tests
run: "cargo update && ./scripts/test.sh"
shell: bash
msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- name: Checkout Sources
uses: actions/checkout@v4
- name: Install Rust Nightly
uses: dtolnay/rust-toolchain@nightly
- name: Downgrade Dependencies to Minimal Versions
run: cargo update -Z minimal-versions
- name: Install MSRV (${{ env.MSRV }})
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MSRV }}
- name: Run Tests
run: ./scripts/test.sh
+13
View File
@@ -0,0 +1,13 @@
language: rust
matrix:
include:
- rust: stable
- rust: beta
- rust: nightly
script:
- ./scripts/test.sh
notifications:
email:
on_success: never
+273
View File
@@ -0,0 +1,273 @@
# Version 0.18
## Version 0.18.1 (Mar 25, 2024)
### New Features
* Added support for the draft `Partitioned` attribute.
The new `CookieBuilder::partition()`, `Cookie::partitioned()`, and
`Cookie::set_partitioned()` methods allow enabling and/or disabling the
attribute. Additionally, the attribute is recognized during parsing.
* Added `CookieBuilder::removal()`, counterpart to `Cookie::make_removal()`.
## Version 0.18.0 (Oct 9, 2023)
### Breaking Changes
* The MSRV is now 1.56.
* `Cookie::value()` no longer trims surrounding double quotes. (89eddd)
Use `Cookie::value_trimmed()` for the previous behavior.
* Many methods now expect a `T: Into<Cookie>` in place of `Cookie`. (49ff7b)
Functions and methods that previously accepted a `Cookie` now accept any `T:
Into<Cookie>`. This particularly affects the `CookieJar` API, which now allows
simpler addition and removal of cookies:
* `jar.add(("foo", "bar"));`
* `jar.add(Cookie::build(("foo", "bar")).path("/"));`
* `jar.remove("foo");`
* `jar.remove(Cookie::build("foo").path("/"));`
* `CookieJar::force_remove()` now expects a `T: AsRef<str>` in place of
`&Cookie`.
Force-removal never requires more information than a cookie's name. The API
has been simplified to reflect this.
* `CookieBuilder::finish()` was deprecated in favor of
`CookieBuilder::build()`.
This largely serves as a compile-time notice that calling `finish()` or
`build()` is largely unnecessary given that `CookieBuilder` implements
`Into<Cookie>`.
* `Cookie::named()` was deprecated in favor of using `Cookie::build()` or
`Cookie::from()`.
* `Cookie::named("foo")` is equivalent to `Cookie::from("foo")`.
* `Cookie::build("foo")` begins building a cookie equivalent to
`Cookie::named("foo")`.
### New Features
* Added `Cookie::value_trimmed()` and `Cookie::name_value_trimmed()`.
These versions of `Cookie::value()` and `Cookie::name_value()`,
respectively, trim a matching pair of surrounding double quotes from the
cookie's value, if any are present.
* String-like types, tuples of string-like types, and `CookieBuilder`
implement `Into<Cookie>`.
Implementations of `Into<Cookie>` for string-like types (`&str`, `String`,
`Cow<str>`), tuples of string-like types `(name: string, value: string)`,
and `CookieBuilder` were added. The former implementations create a cookie
with a name corresponding to the string and an empty value. The tuple
implementation creates a cookie with the given name and value strings. The
`CookieBuilder` implementation returns the built cookie.
* `Key` implements `Debug`.
To not leak sensitive information, the representation is simply `"Key"`.
* `CookieBuilder` implements `Borrow{Mut}<Cookie>`, `As{Ref,Mut}<Cookie>`,
`Display`.
* Added `CookieBuilder::inner{_mut}()` to (mutably) borrow cookies being
built.
* Added `PrefixedJar` and `CookieJar::prefixed{_mut}()`, which implement the
cookie prefixes HTTP draft.
## Version 0.18.0.rc.0 (Sep 27, 2023)
See the entry above for 0.18.0.
# Version 0.17
## Version 0.17.0 (Jan 22, 2022)
### Breaking Changes
* Cookie parsing no longer removes a `.` `Domain` prefix. `Cookie::domain()`
now removes a `.` prefix before returning.
As these changes are inverses, they are not likely observable. The change
only affects manually set `domain` values via the `.domain()` builder
method, the `set_domain()` setter method, or similar, which will now have a
prefix of `.` removed when returned by `Cookie::domain()`. This results in
more consistent treatment of `Domain` values.
### New Features
* Added `Cookie::split_parse()` and `Cookie::split_parse_encoded()` methods.
The methods split a `;`-joined cookie string and parse/decode the split
values. They return a newly introduced iterator value of type `SplitCookies`
over the parse results.
### General Changes and Fixes
* Parsing fuzzers were introduced and run for 48 CPU hours without failure.
* `base64` was updated to `0.21`.
# Version 0.16
## Version 0.16.2 (Dec 16, 2022)
### General Changes
* `base64` was updated to `0.20`.
## Version 0.16.1 (Sep 25, 2022)
### Changes and Fixes
* The `,`, `(`, and `)` are percent-encoded/decoded when encoding is used.
* The `aes-gcm` dependency was updated to 0.10.
## Version 0.16.0 (Dec 28, 2021)
### Breaking Changes
* The MSRV is now `1.53`, up from `1.41` in `0.15`.
* `time` has been updated to `0.3` and is reexported from the crate root.
### General Changes
* `rust-crypto` dependencies were updated to their latest versions.
# Version 0.15
## Version 0.15.1 (Jul 14, 2021)
### Changes and Fixes
* A panic that could result from non-char boundary indexing was fixed.
* Stale doc references to version `0.14` were updated.
## Version 0.15.0 (Feb 25, 2021)
### Breaking Changes
* `Cookie::force_remove()` takes `&Cookie` instead of `Cookie`.
* Child jar methods split into immutable and mutable versions
(`Cookie::{private{_mut}, signed{_mut}}`).
* `Cookie::encoded()` returns a new `Display` struct.
* Dates with year `<= 99` are handled like Chrome: range `0..=68` maps to
`2000..=2068`, `69..=99` to `1969..=1999`.
* `Cookie::{set_}expires()` operates on a new `Expiration` enum.
### New Features
* Added `Cookie::make_removal()` to manually create expired cookies.
* Added `Cookie::stripped()` display variant to print only the `name` and
`value` of a cookie.
* `Key` implements a constant-time `PartialEq`.
* Added `Key::master()` to retrieve the full 512-bit master key.
* Added `PrivateJar::decrypt()` to manually decrypt an encrypted `Cookie`.
* Added `SignedJar::verify()` to manually verify a signed `Cookie`.
* `Cookie::expires()` returns an `Option<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.
+101
View File
@@ -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.18.1"
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.22"
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",
]
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "cookie"
version = "0.18.1"
authors = ["Sergio Benitez <sb@sergio.bz>", "Alex Crichton <alex@alexcrichton.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
repository = "https://github.com/SergioBenitez/cookie-rs"
documentation = "https://docs.rs/cookie"
readme = "README.md"
build = "build.rs"
description = """
HTTP cookie parsing and cookie jar management. Supports signed and private
(encrypted, authenticated) jars.
"""
[features]
percent-encode = ["percent-encoding"]
secure = ["private", "signed", "key-expansion"]
private = ["aes-gcm", "base64", "rand", "subtle"]
signed = ["hmac", "sha2", "base64", "rand", "subtle"]
key-expansion = ["sha2", "hkdf"]
[dependencies]
time = { version = "0.3", default-features = false, features = ["std", "parsing", "formatting", "macros"] }
percent-encoding = { version = "2.0", optional = true }
# dependencies for secure (private/signed) functionality
aes-gcm = { version = "0.10.0", optional = true }
hmac = { version = "0.12.0", optional = true }
sha2 = { version = "0.10.0", optional = true }
base64 = { version = "0.22", optional = true }
rand = { version = "0.8", optional = true }
hkdf = { version = "0.12.0", optional = true }
subtle = { version = "2.3", optional = true }
[build-dependencies]
version_check = "0.9.4"
[package.metadata.docs.rs]
all-features = true
+202
View File
@@ -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.
+26
View File
@@ -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.
+41
View File
@@ -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.18"
```
See the [documentation](http://docs.rs/cookie) for detailed usage information.
# Minimum Supported Rust Version (MSRV)
| Version Range | MSRV |
|----------------------------------|--------------|
| `cookie` `==` `0.18` | `rustc 1.56` |
| `0.16` `<=` `cookie` `<=` `0.17` | `rustc 1.53` |
| `cookie` `<=` `0.15` | `rustc 1.41` |
# License
This project is licensed under your option of either of the following:
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
http://opensource.org/licenses/MIT)
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in `cookie` by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
+5
View File
@@ -0,0 +1,5 @@
fn main() {
if let Some(true) = version_check::supports_feature("doc_cfg") {
println!("cargo:rustc-cfg=nightly");
}
}
+19
View File
@@ -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
+411
View File
@@ -0,0 +1,411 @@
use std::borrow::{Cow, Borrow, BorrowMut};
use crate::{Cookie, SameSite, Expiration};
/// Structure that follows the builder pattern for building `Cookie` structs.
///
/// To construct a cookie:
///
/// 1. Call [`Cookie::build()`] to start building.
/// 2. Use any of the builder methods to set fields in the cookie.
///
/// The resulting `CookieBuilder` can be passed directly into methods expecting
/// a `T: Into<Cookie>`:
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
///
/// let mut jar = CookieJar::new();
/// jar.add(Cookie::build(("key", "value")).secure(true).path("/"));
/// jar.remove(Cookie::build("key").path("/"));
/// ```
///
/// You can also call [`CookieBuilder::build()`] directly to get a `Cookie`:
///
/// ```rust
/// use cookie::Cookie;
/// use cookie::time::Duration;
///
/// let cookie: Cookie = Cookie::build(("name", "value"))
/// .domain("www.rust-lang.org")
/// .path("/")
/// .secure(true)
/// .http_only(true)
/// .max_age(Duration::days(1))
/// .build();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct CookieBuilder<'c> {
/// The cookie being built.
cookie: Cookie<'c>,
}
impl<'c> CookieBuilder<'c> {
/// Creates a new `CookieBuilder` instance from the given name and value.
///
/// This method is typically called indirectly via [`Cookie::build()`].
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// // These two snippets are equivalent:
///
/// let c = Cookie::build(("foo", "bar"));
/// assert_eq!(c.inner().name_value(), ("foo", "bar"));
///
/// let c = Cookie::new("foo", "bar");
/// assert_eq!(c.name_value(), ("foo", "bar"));
/// ```
pub fn new<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.
///
/// See [`Expiration`] for conversions.
///
/// # Example
///
/// ```rust
/// # extern crate cookie;
/// use cookie::{Cookie, Expiration};
/// use cookie::time::OffsetDateTime;
///
/// # fn main() {
/// let c = Cookie::build(("foo", "bar")).expires(OffsetDateTime::now_utc());
/// assert!(c.inner().expires().is_some());
///
/// let c = Cookie::build(("foo", "bar")).expires(None);
/// assert_eq!(c.inner().expires(), Some(Expiration::Session));
/// # }
/// ```
#[inline]
pub fn expires<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
/// use cookie::Cookie;
/// use cookie::time::Duration;
///
/// let c = Cookie::build(("foo", "bar")).max_age(Duration::minutes(30));
/// assert_eq!(c.inner().max_age(), Some(Duration::seconds(30 * 60)));
/// ```
#[inline]
pub fn max_age(mut self, value: time::Duration) -> Self {
self.cookie.set_max_age(value);
self
}
/// Sets the `domain` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// let c = Cookie::build(("foo", "bar")).domain("www.rust-lang.org");
/// assert_eq!(c.inner().domain(), Some("www.rust-lang.org"));
/// ```
pub fn domain<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("/");
/// assert_eq!(c.inner().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);
/// assert_eq!(c.inner().secure(), Some(true));
/// ```
#[inline]
pub fn secure(mut self, value: bool) -> Self {
self.cookie.set_secure(value);
self
}
/// Sets the `http_only` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// let c = Cookie::build(("foo", "bar")).http_only(true);
/// assert_eq!(c.inner().http_only(), Some(true));
/// ```
#[inline]
pub fn http_only(mut self, value: bool) -> Self {
self.cookie.set_http_only(value);
self
}
/// Sets the `same_site` field in the cookie being built.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, SameSite};
///
/// let c = Cookie::build(("foo", "bar")).same_site(SameSite::Strict);
/// assert_eq!(c.inner().same_site(), Some(SameSite::Strict));
/// ```
#[inline]
pub fn same_site(mut self, value: SameSite) -> Self {
self.cookie.set_same_site(value);
self
}
/// Sets the `partitioned` field in the cookie being built.
///
/// **Note:** _Partitioned_ cookies require the `Secure` attribute to be
/// set. As such, `Partitioned` cookies are always rendered with the
/// `Secure` attribute, irrespective of the `Secure` attribute's setting.
///
/// **Note:** This cookie attribute is an [HTTP draft]! Its meaning and
/// definition are not standardized and therefore subject to change.
///
/// [HTTP draft]: https://www.ietf.org/id/draft-cutler-httpbis-partitioned-cookies-01.html
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// let c = Cookie::build(("foo", "bar")).partitioned(true);
/// assert_eq!(c.inner().partitioned(), Some(true));
/// assert!(c.to_string().contains("Secure"));
/// ```
#[inline]
pub fn partitioned(mut self, value: bool) -> Self {
self.cookie.set_partitioned(value);
self
}
/// Makes the cookie being built 'permanent' by extending its expiration and
/// max age 20 years into the future. See also [`Cookie::make_permanent()`].
///
/// # Example
///
/// ```rust
/// # extern crate cookie;
/// use cookie::Cookie;
/// use cookie::time::Duration;
///
/// # fn main() {
/// let c = Cookie::build(("foo", "bar")).permanent();
/// assert_eq!(c.inner().max_age(), Some(Duration::days(365 * 20)));
/// # assert!(c.inner().expires().is_some());
/// # }
/// ```
#[inline]
pub fn permanent(mut self) -> Self {
self.cookie.make_permanent();
self
}
/// Makes the cookie being built 'removal' by clearing its value, setting a
/// max-age of `0`, and setting an expiration date far in the past. See also
/// [`Cookie::make_removal()`].
///
/// # Example
///
/// ```rust
/// # extern crate cookie;
/// use cookie::Cookie;
/// use cookie::time::Duration;
///
/// # fn main() {
/// let mut builder = Cookie::build("foo").removal();
/// assert_eq!(builder.inner().max_age(), Some(Duration::ZERO));
///
/// let mut builder = Cookie::build(("name", "value")).removal();
/// assert_eq!(builder.inner().value(), "");
/// assert_eq!(builder.inner().max_age(), Some(Duration::ZERO));
/// # }
/// ```
#[inline]
pub fn removal(mut self) -> Self {
self.cookie.make_removal();
self
}
/// Returns a borrow to the cookie currently being built.
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// let builder = Cookie::build(("name", "value"))
/// .domain("www.rust-lang.org")
/// .path("/")
/// .http_only(true);
///
/// assert_eq!(builder.inner().name_value(), ("name", "value"));
/// assert_eq!(builder.inner().domain(), Some("www.rust-lang.org"));
/// assert_eq!(builder.inner().path(), Some("/"));
/// assert_eq!(builder.inner().http_only(), Some(true));
/// assert_eq!(builder.inner().secure(), None);
/// ```
#[inline]
pub fn inner(&self) -> &Cookie<'c> {
&self.cookie
}
/// Returns a mutable borrow to the cookie currently being built.
///
/// # Example
///
/// ```rust
/// use cookie::Cookie;
///
/// let mut builder = Cookie::build(("name", "value"))
/// .domain("www.rust-lang.org")
/// .path("/")
/// .http_only(true);
///
/// assert_eq!(builder.inner().http_only(), Some(true));
///
/// builder.inner_mut().set_http_only(false);
/// assert_eq!(builder.inner().http_only(), Some(false));
/// ```
#[inline]
pub fn inner_mut(&mut self) -> &mut Cookie<'c> {
&mut self.cookie
}
/// Finishes building and returns the built `Cookie`.
///
/// This method usually does not need to be called directly. This is because
/// `CookieBuilder` implements `Into<Cookie>`, so a value of `CookieBuilder`
/// can be passed directly into any method that expects a `C: Into<Cookie>`.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
///
/// // We don't usually need to use `build()`. Inspect with `inner()`, and
/// // pass the builder directly into methods expecting `T: Into<Cookie>`.
/// let c = Cookie::build(("foo", "bar"))
/// .domain("crates.io")
/// .path("/");
///
/// // Use `inner()` and inspect the cookie.
/// assert_eq!(c.inner().name_value(), ("foo", "bar"));
/// assert_eq!(c.inner().domain(), Some("crates.io"));
/// assert_eq!(c.inner().path(), Some("/"));
///
/// // Add the cookie to a jar. Note the automatic conversion.
/// CookieJar::new().add(c);
///
/// // We could use `build()` to get a `Cookie` when needed.
/// let c = Cookie::build(("foo", "bar"))
/// .domain("crates.io")
/// .path("/")
/// .build();
///
/// // Inspect the built cookie.
/// assert_eq!(c.name_value(), ("foo", "bar"));
/// assert_eq!(c.domain(), Some("crates.io"));
/// assert_eq!(c.path(), Some("/"));
///
/// // Add the cookie to a jar.
/// CookieJar::new().add(c);
/// ```
#[inline]
pub fn build(self) -> Cookie<'c> {
self.cookie
}
/// Deprecated. Convert `self` into a `Cookie`.
///
/// Instead of using this method, pass a `CookieBuilder` directly into
/// methods expecting a `T: Into<Cookie>`. For other cases, use
/// [`CookieBuilder::build()`].
#[deprecated(since="0.18.0", note="`CookieBuilder` can be passed in to methods expecting a `Cookie`; for other cases, use `CookieBuilder::build()`")]
pub fn finish(self) -> Cookie<'c> {
self.cookie
}
}
impl std::fmt::Display for CookieBuilder<'_> {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.cookie.fmt(f)
}
}
// NOTE: We don't implement `Deref` or `DerefMut` because there are tons of name
// collisions with builder methods.
impl<'a> Borrow<Cookie<'a>> for CookieBuilder<'a> {
fn borrow(&self) -> &Cookie<'a> {
&self.cookie
}
}
impl<'a> BorrowMut<Cookie<'a>> for CookieBuilder<'a> {
fn borrow_mut(&mut self) -> &mut Cookie<'a> {
&mut self.cookie
}
}
impl<'a> AsRef<Cookie<'a>> for CookieBuilder<'a> {
fn as_ref(&self) -> &Cookie<'a> {
&self.cookie
}
}
impl<'a> AsMut<Cookie<'a>> for CookieBuilder<'a> {
fn as_mut(&mut self) -> &mut Cookie<'a> {
&mut self.cookie
}
}
impl<'a, 'b> PartialEq<Cookie<'b>> for CookieBuilder<'a> {
fn eq(&self, other: &Cookie<'b>) -> bool {
&self.cookie == other
}
}
impl<'a, 'b> PartialEq<CookieBuilder<'b>> for Cookie<'a> {
fn eq(&self, other: &CookieBuilder<'b>) -> bool {
self == &other.cookie
}
}
impl<'c> From<Cookie<'c>> for CookieBuilder<'c> {
fn from(cookie: Cookie<'c>) -> Self {
CookieBuilder { cookie }
}
}
+65
View File
@@ -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(crate) struct DeltaCookie {
pub cookie: Cookie<'static>,
pub removed: bool,
}
impl DeltaCookie {
/// Create a new `DeltaCookie` that is being added to a jar.
#[inline]
pub fn added(cookie: Cookie<'static>) -> DeltaCookie {
DeltaCookie { cookie, removed: false, }
}
/// Create a new `DeltaCookie` that is being removed from a jar. The
/// `cookie` should be a "removal" cookie.
#[inline]
pub fn removed(cookie: Cookie<'static>) -> DeltaCookie {
DeltaCookie { cookie, removed: true, }
}
}
impl Deref for DeltaCookie {
type Target = Cookie<'static>;
fn deref(&self) -> &Cookie<'static> {
&self.cookie
}
}
impl DerefMut for DeltaCookie {
fn deref_mut(&mut self) -> &mut Cookie<'static> {
&mut self.cookie
}
}
impl PartialEq for DeltaCookie {
fn eq(&self, other: &DeltaCookie) -> bool {
self.name() == other.name()
}
}
impl Eq for DeltaCookie {}
impl Hash for DeltaCookie {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name().hash(state);
}
}
impl Borrow<str> for DeltaCookie {
fn borrow(&self) -> &str {
self.name()
}
}
+136
View File
@@ -0,0 +1,136 @@
use time::OffsetDateTime;
/// A cookie's expiration: either a date-time or session.
///
/// An `Expiration` is constructible with `Expiration::from()` via any of:
///
/// * `None` -> `Expiration::Session`
/// * `Some(OffsetDateTime)` -> `Expiration::DateTime`
/// * `OffsetDateTime` -> `Expiration::DateTime`
///
/// ```rust
/// use cookie::Expiration;
/// use time::OffsetDateTime;
///
/// let expires = Expiration::from(None);
/// assert_eq!(expires, Expiration::Session);
///
/// let now = OffsetDateTime::now_utc();
/// let expires = Expiration::from(now);
/// assert_eq!(expires, Expiration::DateTime(now));
///
/// let expires = Expiration::from(Some(now));
/// assert_eq!(expires, Expiration::DateTime(now));
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Expiration {
/// Expiration for a "permanent" cookie at a specific date-time.
DateTime(OffsetDateTime),
/// Expiration for a "session" cookie. Browsers define the notion of a
/// "session" and will automatically expire session cookies when they deem
/// the "session" to be over. This is typically, but need not be, when the
/// browser is closed.
Session,
}
impl Expiration {
/// Returns `true` if `self` is an `Expiration::DateTime`.
///
/// # Example
///
/// ```rust
/// use cookie::Expiration;
/// use time::OffsetDateTime;
///
/// let expires = Expiration::from(None);
/// assert!(!expires.is_datetime());
///
/// let expires = Expiration::from(OffsetDateTime::now_utc());
/// assert!(expires.is_datetime());
/// ```
pub fn is_datetime(&self) -> bool {
match self {
Expiration::DateTime(_) => true,
Expiration::Session => false
}
}
/// Returns `true` if `self` is an `Expiration::Session`.
///
/// # Example
///
/// ```rust
/// use cookie::Expiration;
/// use time::OffsetDateTime;
///
/// let expires = Expiration::from(None);
/// assert!(expires.is_session());
///
/// let expires = Expiration::from(OffsetDateTime::now_utc());
/// assert!(!expires.is_session());
/// ```
pub fn is_session(&self) -> bool {
match self {
Expiration::DateTime(_) => false,
Expiration::Session => true
}
}
/// Returns the inner `OffsetDateTime` if `self` is a `DateTime`.
///
/// # Example
///
/// ```rust
/// use cookie::Expiration;
/// use time::OffsetDateTime;
///
/// let expires = Expiration::from(None);
/// assert!(expires.datetime().is_none());
///
/// let now = OffsetDateTime::now_utc();
/// let expires = Expiration::from(now);
/// assert_eq!(expires.datetime(), Some(now));
/// ```
pub fn datetime(self) -> Option<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
}
}
}
+812
View File
@@ -0,0 +1,812 @@
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::prefix::{Prefix, PrefixedJar};
use crate::Cookie;
/// A collection of cookies that tracks its modifications.
///
/// A `CookieJar` provides storage for any number of cookies. Any changes made
/// to the jar are tracked; the changes can be retrieved via the
/// [`delta`](#method.delta) method which returns an iterator over the changes.
///
/// # Usage
///
/// A jar's life begins via [`CookieJar::new()`] and calls to
/// [`add_original()`](#method.add_original):
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(("name", "value"));
/// jar.add_original(("second", "another"));
/// jar.add_original(Cookie::build(("third", "again")).path("/"));
/// ```
///
/// Cookies can be added via [`CookieJar::add()`] and removed via
/// [`CookieJar::remove()`]. Note that any `T: Into<Cookie>` can be passed into
/// these methods; see [`Cookie::build()`] for a table of implementing types.
///
/// Finally, cookies can be retrieved with [`CookieJar::get()`].
///
/// ```rust
/// # use cookie::{Cookie, CookieJar};
/// let mut jar = CookieJar::new();
/// jar.add(("a", "one"));
/// jar.add(("b", "two"));
///
/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one"));
/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two"));
///
/// jar.remove("b");
/// assert!(jar.get("b").is_none());
/// ```
///
/// # Deltas
///
/// A jar keeps track of any modifications made to it over time. The
/// modifications are recorded as cookies. The modifications can be retrieved
/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add`
/// results in the same `Cookie` appearing in the `delta`; cookies added via
/// `add_original` do not count towards the delta. Any _original_ cookie that is
/// removed from a jar results in a "removal" cookie appearing in the delta. A
/// "removal" cookie is a cookie that a server sends so that the cookie is
/// removed from the client's machine.
///
/// Deltas are typically used to create `Set-Cookie` headers corresponding to
/// the changes made to a cookie jar over a period of time.
///
/// ```rust
/// # use cookie::{Cookie, CookieJar};
/// let mut jar = CookieJar::new();
///
/// // original cookies don't affect the delta
/// jar.add_original(("original", "value"));
/// assert_eq!(jar.delta().count(), 0);
///
/// // new cookies result in an equivalent `Cookie` in the delta
/// jar.add(("a", "one"));
/// jar.add(("b", "two"));
/// assert_eq!(jar.delta().count(), 2);
///
/// // removing an original cookie adds a "removal" cookie to the delta
/// jar.remove("original");
/// assert_eq!(jar.delta().count(), 3);
///
/// // removing a new cookie that was added removes that `Cookie` from the delta
/// jar.remove("a");
/// assert_eq!(jar.delta().count(), 2);
/// ```
#[derive(Default, Debug, Clone)]
pub struct CookieJar {
original_cookies: HashSet<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(("name", "value"));
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// ```
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
self.delta_cookies
.get(name)
.or_else(|| self.original_cookies.get(name))
.and_then(|c| if c.removed { None } else { Some(&c.cookie) })
}
/// Adds an "original" `cookie` to this jar. If an original cookie with the
/// same name already exists, it is replaced with `cookie`. Cookies added
/// with `add` take precedence and are not replaced by this method.
///
/// Adding an original cookie does not affect the [delta](#method.delta)
/// computation. This method is intended to be used to seed the cookie jar
/// with cookies received from a client's HTTP message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(("name", "value"));
/// jar.add_original(("second", "two"));
///
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
/// assert_eq!(jar.iter().count(), 2);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.original_cookies.replace(DeltaCookie::added(cookie.into()));
}
/// Adds `cookie` to this jar. If a cookie with the same name already
/// exists, it is replaced with `cookie`.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add(("name", "value"));
/// jar.add(("second", "two"));
///
/// assert_eq!(jar.get("name").map(|c| c.value()), Some("value"));
/// assert_eq!(jar.get("second").map(|c| c.value()), Some("two"));
/// assert_eq!(jar.iter().count(), 2);
/// assert_eq!(jar.delta().count(), 2);
/// ```
pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.delta_cookies.replace(DeltaCookie::added(cookie.into()));
}
/// Removes `cookie` from this jar. If an _original_ cookie with the same
/// name as `cookie` is present in the jar, a _removal_ cookie will be
/// present in the `delta` computation. **To properly generate the removal
/// cookie, `cookie` must contain the same `path` and `domain` as the cookie
/// that was initially set.**
///
/// A "removal" cookie is a cookie that has the same name as the original
/// cookie but has an empty value, a max-age of 0, and an expiration date
/// far in the past. See also [`Cookie::make_removal()`].
///
/// # Example
///
/// Removing an _original_ cookie results in a _removal_ cookie:
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
/// use cookie::time::Duration;
///
/// let mut jar = CookieJar::new();
///
/// // Assume this cookie originally had a path of "/" and domain of "a.b".
/// jar.add_original(("name", "value"));
///
/// // If the path and domain were set, they must be provided to `remove`.
/// jar.remove(Cookie::build("name").path("/").domain("a.b"));
///
/// // The delta will contain the removal cookie.
/// let delta: Vec<_> = jar.delta().collect();
/// assert_eq!(delta.len(), 1);
/// assert_eq!(delta[0].name(), "name");
/// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0)));
/// ```
///
/// Removing a new cookie does not result in a _removal_ cookie unless
/// there's an original cookie with the same name:
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add(("name", "value"));
/// assert_eq!(jar.delta().count(), 1);
///
/// jar.remove("name");
/// assert_eq!(jar.delta().count(), 0);
///
/// jar.add_original(("name", "value"));
/// jar.add(("name", "value"));
/// assert_eq!(jar.delta().count(), 1);
///
/// jar.remove("name");
/// assert_eq!(jar.delta().count(), 1);
/// ```
pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
if self.original_cookies.contains(cookie.name()) {
cookie.make_removal();
self.delta_cookies.replace(DeltaCookie::removed(cookie));
} else {
self.delta_cookies.remove(cookie.name());
}
}
/// Removes `cookie` from this jar completely.
///
/// This method differs from `remove` in that no delta cookie is created
/// under any condition. Thus, no path or domain are needed: only the
/// cookie's name. Neither the `delta` nor `iter` methods will return a
/// cookie that is removed using this method.
///
/// # Example
///
/// Removing an _original_ cookie; no _removal_ cookie is generated:
///
/// ```rust
/// # extern crate cookie;
/// use cookie::{CookieJar, Cookie};
/// use cookie::time::Duration;
///
/// # fn main() {
/// let mut jar = CookieJar::new();
///
/// // Add an original cookie and a new cookie.
/// jar.add_original(("name", "value"));
/// jar.add(("key", "value"));
/// assert_eq!(jar.delta().count(), 1);
/// assert_eq!(jar.iter().count(), 2);
///
/// // Now force remove the original cookie.
/// jar.force_remove("name");
/// assert_eq!(jar.delta().count(), 1);
/// assert_eq!(jar.iter().count(), 1);
///
/// // Now force remove the new cookie. `to_string()` for illustration only.
/// jar.force_remove("key".to_string());
/// assert_eq!(jar.delta().count(), 0);
/// assert_eq!(jar.iter().count(), 0);
/// # }
/// ```
pub fn force_remove<N: AsRef<str>>(&mut self, name: N) {
self.original_cookies.remove(name.as_ref());
self.delta_cookies.remove(name.as_ref());
}
/// Removes all delta cookies, i.e. all cookies not added via
/// [`CookieJar::add_original()`], from this `CookieJar`. This undoes any
/// changes from [`CookieJar::add()`] and [`CookieJar::remove()`]
/// operations.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
///
/// // Only original cookies will remain after calling `reset_delta`.
/// jar.add_original(("name", "value"));
/// jar.add_original(("language", "Rust"));
///
/// // These operations, represented by delta cookies, will be reset.
/// jar.add(("language", "C++"));
/// jar.remove("name");
///
/// // All is normal.
/// assert_eq!(jar.get("name"), None);
/// assert_eq!(jar.get("language").map(Cookie::value), Some("C++"));
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 2);
///
/// // Resetting undoes delta operations.
/// jar.reset_delta();
/// assert_eq!(jar.get("name").map(Cookie::value), Some("value"));
/// assert_eq!(jar.get("language").map(Cookie::value), Some("Rust"));
/// assert_eq!(jar.iter().count(), 2);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn reset_delta(&mut self) {
self.delta_cookies = HashSet::new();
}
/// Returns an iterator over cookies that represent the changes to this jar
/// over time. These cookies can be rendered directly as `Set-Cookie` header
/// values to affect the changes made to this jar on the client.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
/// jar.add_original(("name", "value"));
/// jar.add_original(("second", "two"));
///
/// // Add new cookies.
/// jar.add(("new", "third"));
/// jar.add(("another", "fourth"));
/// jar.add(("yac", "fifth"));
///
/// // Remove some cookies.
/// jar.remove(("name"));
/// jar.remove(("another"));
///
/// // Delta contains two new cookies ("new", "yac") and a removal ("name").
/// assert_eq!(jar.delta().count(), 3);
/// ```
pub fn delta(&self) -> Delta {
Delta { iter: self.delta_cookies.iter() }
}
/// Returns an iterator over all of the cookies present in this jar.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie};
///
/// let mut jar = CookieJar::new();
///
/// jar.add_original(("name", "value"));
/// jar.add_original(("second", "two"));
///
/// jar.add(("new", "third"));
/// jar.add(("another", "fourth"));
/// jar.add(("yac", "fifth"));
///
/// jar.remove("name");
/// jar.remove("another");
///
/// // There are three cookies in the jar: "second", "new", and "yac".
/// # assert_eq!(jar.iter().count(), 3);
/// for cookie in jar.iter() {
/// match cookie.name() {
/// "second" => assert_eq!(cookie.value(), "two"),
/// "new" => assert_eq!(cookie.value(), "third"),
/// "yac" => assert_eq!(cookie.value(), "fifth"),
/// _ => unreachable!("there are only three cookies in the jar")
/// }
/// }
/// ```
pub fn iter(&self) -> Iter {
Iter {
delta_cookies: self.delta_cookies.iter()
.chain(self.original_cookies.difference(&self.delta_cookies)),
}
}
/// Returns a read-only `PrivateJar` with `self` as its parent jar using the
/// key `key` to verify/decrypt cookies retrieved from the child jar. Any
/// retrievals from the child jar will be made from the parent jar.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a private (signed + encrypted) cookie.
/// let mut jar = CookieJar::new();
/// jar.private_mut(&key).add(("private", "text"));
///
/// // The cookie's contents are encrypted.
/// assert_ne!(jar.get("private").unwrap().value(), "text");
///
/// // They can be decrypted and verified through the child jar.
/// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text");
///
/// // A tampered with cookie does not validate but still exists.
/// let mut cookie = jar.get("private").unwrap().clone();
/// jar.add(("private", cookie.value().to_string() + "!"));
/// assert!(jar.private(&key).get("private").is_none());
/// assert!(jar.get("private").is_some());
/// ```
#[cfg(feature = "private")]
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))]
pub fn private<'a>(&'a self, key: &Key) -> PrivateJar<&'a Self> {
PrivateJar::new(self, key)
}
/// Returns a read/write `PrivateJar` with `self` as its parent jar using
/// the key `key` to sign/encrypt and verify/decrypt cookies added/retrieved
/// from the child jar.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a private (signed + encrypted) cookie.
/// let mut jar = CookieJar::new();
/// jar.private_mut(&key).add(("private", "text"));
///
/// // Remove a cookie using the child jar.
/// jar.private_mut(&key).remove("private");
/// ```
#[cfg(feature = "private")]
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))]
pub fn private_mut<'a>(&'a mut self, key: &Key) -> PrivateJar<&'a mut Self> {
PrivateJar::new(self, key)
}
/// Returns a read-only `SignedJar` with `self` as its parent jar using the
/// key `key` to verify cookies retrieved from the child jar. Any retrievals
/// from the child jar will be made from the parent jar.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a signed cookie.
/// let mut jar = CookieJar::new();
/// jar.signed_mut(&key).add(("signed", "text"));
///
/// // The cookie's contents are signed but still in plaintext.
/// assert_ne!(jar.get("signed").unwrap().value(), "text");
/// assert!(jar.get("signed").unwrap().value().contains("text"));
///
/// // They can be verified through the child jar.
/// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text");
///
/// // A tampered with cookie does not validate but still exists.
/// let mut cookie = jar.get("signed").unwrap().clone();
/// jar.add(("signed", cookie.value().to_string() + "!"));
/// assert!(jar.signed(&key).get("signed").is_none());
/// assert!(jar.get("signed").is_some());
/// ```
#[cfg(feature = "signed")]
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
pub fn signed<'a>(&'a self, key: &Key) -> SignedJar<&'a Self> {
SignedJar::new(self, key)
}
/// Returns a read/write `SignedJar` with `self` as its parent jar using the
/// key `key` to sign/verify cookies added/retrieved from the child jar.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Key};
///
/// // Generate a secure key.
/// let key = Key::generate();
///
/// // Add a signed cookie.
/// let mut jar = CookieJar::new();
/// jar.signed_mut(&key).add(("signed", "text"));
///
/// // Remove a cookie.
/// jar.signed_mut(&key).remove("signed");
/// ```
#[cfg(feature = "signed")]
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
pub fn signed_mut<'a>(&'a mut self, key: &Key) -> SignedJar<&'a mut Self> {
SignedJar::new(self, key)
}
/// Returns a read-only `PrefixedJar` with `self` as its parent jar that
/// prefixes the name of cookies with `prefix`. Any retrievals from the
/// child jar will be made from the parent jar.
///
/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning
/// and definition are subject to change.
///
/// # Example
///
/// ```rust
/// use cookie::CookieJar;
/// use cookie::prefix::{Host, Secure};
///
/// // Add a `Host` prefixed cookie.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Host).add(("h0st", "value"));
/// jar.prefixed_mut(Secure).add(("secur3", "value"));
///
/// // The cookie's name is prefixed in the parent jar.
/// assert!(matches!(jar.get("h0st"), None));
/// assert!(matches!(jar.get("__Host-h0st"), Some(_)));
/// assert!(matches!(jar.get("secur3"), None));
/// assert!(matches!(jar.get("__Secure-secur3"), Some(_)));
///
/// // The prefixed jar automatically removes the prefix.
/// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st");
/// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value");
/// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().name(), "secur3");
/// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().value(), "value");
///
/// // Only the correct prefixed jar retrieves the cookie.
/// assert!(matches!(jar.prefixed(Host).get("secur3"), None));
/// assert!(matches!(jar.prefixed(Secure).get("h0st"), None));
/// ```
#[inline(always)]
pub fn prefixed<'a, P: Prefix>(&'a self, prefix: P) -> PrefixedJar<P, &'a Self> {
let _ = prefix;
PrefixedJar::new(self)
}
/// Returns a read/write `PrefixedJar` with `self` as its parent jar that
/// prefixes the name of cookies with `prefix` and makes the cookie conform
/// to the prefix's requirements. This means that added cookies:
///
/// 1. Have the [`Prefix::PREFIX`] prepended to their name.
/// 2. Modify the cookie via [`Prefix::conform()`] so that it conforms to
/// the prefix's requirements.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning
/// and definition are subject to change.
///
/// # Example
///
/// ```rust
/// use cookie::CookieJar;
/// use cookie::prefix::{Host, Secure};
///
/// // Add some prefixed cookies.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Host).add(("one", "1"));
/// jar.prefixed_mut(Secure).add((2.to_string(), "2"));
/// jar.prefixed_mut(Host).add((format!("{:0b}", 3), "0b11"));
///
/// // Fetch cookies with either `prefixed()` or `prefixed_mut()`.
/// assert_eq!(jar.prefixed(Host).get("one").unwrap().value(), "1");
/// assert_eq!(jar.prefixed(Secure).get("2").unwrap().value(), "2");
/// assert_eq!(jar.prefixed_mut(Host).get("11").unwrap().value(), "0b11");
///
/// // Remove cookies.
/// jar.prefixed_mut(Host).remove("one");
/// assert!(jar.prefixed(Host).get("one").is_none());
/// ```
pub fn prefixed_mut<'a, P: Prefix>(&'a mut self, prefix: P) -> PrefixedJar<P, &'a mut Self> {
let _ = prefix;
PrefixedJar::new(self)
}
}
use std::collections::hash_set::Iter as HashSetIter;
/// Iterator over the changes to a cookie jar.
pub struct Delta<'a> {
iter: HashSetIter<'a, DeltaCookie>,
}
impl<'a> Iterator for Delta<'a> {
type Item = &'a Cookie<'static>;
fn next(&mut self) -> Option<&'a Cookie<'static>> {
self.iter.next().map(|c| &c.cookie)
}
}
use std::collections::hash_set::Difference;
use std::collections::hash_map::RandomState;
use std::iter::Chain;
/// Iterator over all of the cookies in a jar.
pub struct Iter<'a> {
delta_cookies: Chain<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(("test", ""));
c.add(("test2", ""));
c.remove("test");
assert!(c.get("test").is_none());
assert!(c.get("test2").is_some());
c.add(("test3", ""));
c.remove("test2");
c.remove("test3");
assert!(c.get("test").is_none());
assert!(c.get("test2").is_none());
assert!(c.get("test3").is_none());
}
#[test]
fn jar_is_send() {
fn is_send<T: Send>(_: T) -> bool {
true
}
assert!(is_send(CookieJar::new()))
}
#[test]
#[cfg(all(feature = "signed", feature = "private"))]
fn iter() {
let key = crate::Key::generate();
let mut c = CookieJar::new();
c.add_original(Cookie::new("original", "original"));
c.add(Cookie::new("test", "test"));
c.add(Cookie::new("test2", "test2"));
c.add(Cookie::new("test3", "test3"));
assert_eq!(c.iter().count(), 4);
c.signed_mut(&key).add(Cookie::new("signed", "signed"));
c.private_mut(&key).add(Cookie::new("encrypted", "encrypted"));
assert_eq!(c.iter().count(), 6);
c.remove("test");
assert_eq!(c.iter().count(), 5);
c.remove("signed");
c.remove("test2");
assert_eq!(c.iter().count(), 3);
c.add(("test2", "test2"));
assert_eq!(c.iter().count(), 4);
c.remove("test2");
assert_eq!(c.iter().count(), 3);
}
#[test]
fn delta() {
use std::collections::HashMap;
use time::Duration;
let mut c = CookieJar::new();
c.add_original(Cookie::new("original", "original"));
c.add_original(Cookie::new("original1", "original1"));
c.add(Cookie::new("test", "test"));
c.add(Cookie::new("test2", "test2"));
c.add(Cookie::new("test3", "test3"));
c.add(Cookie::new("test4", "test4"));
c.remove("test");
c.remove("original");
assert_eq!(c.delta().count(), 4);
let names: HashMap<_, _> = c.delta()
.map(|c| (c.name(), c.max_age()))
.collect();
assert!(names.get("test2").unwrap().is_none());
assert!(names.get("test3").unwrap().is_none());
assert!(names.get("test4").unwrap().is_none());
assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0)));
}
#[test]
fn replace_original() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("original_a", "a"));
jar.add_original(Cookie::new("original_b", "b"));
assert_eq!(jar.get("original_a").unwrap().value(), "a");
jar.add(Cookie::new("original_a", "av2"));
assert_eq!(jar.get("original_a").unwrap().value(), "av2");
}
#[test]
fn empty_delta() {
let mut jar = CookieJar::new();
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
jar.remove("name");
assert_eq!(jar.delta().count(), 0);
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.remove("name");
assert_eq!(jar.delta().count(), 1);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
jar.remove("name");
assert_eq!(jar.delta().count(), 1);
}
#[test]
fn add_remove_add() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.remove("name");
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
// The cookie's been deleted. Another original doesn't change that.
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.remove("name");
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
jar.remove("name");
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().count(), 1);
}
#[test]
fn replace_remove() {
let mut jar = CookieJar::new();
jar.add_original(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 0);
jar.add(Cookie::new("name", "val"));
assert_eq!(jar.delta().count(), 1);
assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1);
jar.remove("name");
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
}
#[test]
fn remove_with_path() {
let mut jar = CookieJar::new();
jar.add_original(("name", "val"));
assert_eq!(jar.iter().count(), 1);
assert_eq!(jar.delta().count(), 0);
assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1);
jar.remove(Cookie::build("name").path("/"));
assert_eq!(jar.iter().count(), 0);
assert_eq!(jar.delta().count(), 1);
assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1);
assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1);
}
}
File diff suppressed because it is too large Load Diff
+515
View File
@@ -0,0 +1,515 @@
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()
}
}
#[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!")
}
// 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) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
None => return Err(ParseError::MissingPair)
};
if name.is_empty() {
return Err(ParseError::EmptyName);
}
// If there is nothing to decode, or we're not decoding, use indexes.
let indexed_names = |s, name, value| {
let name = CookieStr::indexed(name, s).expect("name sub");
let value = CookieStr::indexed(value, s).expect("value sub");
(name, value)
};
// Create a cookie with all of the defaults. We'll fill things in while we
// iterate through the parameters below.
let (name, value) = if decode {
match name_val_decoded(name, value)? {
Some((name, value)) => (name, value),
None => indexed_names(s, name, value)
}
} else {
indexed_names(s, name, value)
};
let mut cookie: Cookie<'c> = Cookie {
name, value,
cookie_string: None,
expires: None,
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
same_site: None,
partitioned: 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(d)) if !d.is_empty() => {
cookie.domain = Some(CookieStr::indexed(d, s).expect("domain sub"));
},
("path", Some(v)) => {
cookie.path = Some(CookieStr::indexed(v, s).expect("path sub"));
},
("samesite", Some(v)) => {
if v.eq_ignore_ascii_case("strict") {
cookie.same_site = Some(SameSite::Strict);
} else if v.eq_ignore_ascii_case("lax") {
cookie.same_site = Some(SameSite::Lax);
} else if v.eq_ignore_ascii_case("none") {
cookie.same_site = Some(SameSite::None);
} else {
// We do nothing here, for now. When/if the `SameSite`
// attribute becomes standard, the spec says that we should
// ignore this cookie, i.e, fail to parse it, when an
// invalid value is passed in. The draft is at
// http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html.
}
}
("partitioned", _) => cookie.partitioned = Some(true),
("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);
assert_eq_parse!("foo=bar; SameSite=Lax", expected);
assert_eq_parse!("foo=bar; SameSite=lax", expected);
assert_eq_parse!("foo=bar; SameSite=LAX", expected);
assert_eq_parse!("foo=bar; samesite=Lax", expected);
assert_eq_parse!("foo=bar; SAMESITE=Lax", expected);
let expected = Cookie::build(("foo", "bar")).same_site(SameSite::Strict);
assert_eq_parse!("foo=bar; SameSite=Strict", expected);
assert_eq_parse!("foo=bar; SameSITE=Strict", expected);
assert_eq_parse!("foo=bar; SameSite=strict", expected);
assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
assert_eq_parse!("foo=bar; SameSite=STRICT", expected);
let expected = Cookie::build(("foo", "bar")).same_site(SameSite::None);
assert_eq_parse!("foo=bar; SameSite=None", expected);
assert_eq_parse!("foo=bar; SameSITE=none", expected);
assert_eq_parse!("foo=bar; SameSite=NOne", expected);
assert_eq_parse!("foo=bar; SameSite=nOne", expected);
}
#[test]
fn parse() {
assert!(Cookie::parse("bar").is_err());
assert!(Cookie::parse("=bar").is_err());
assert!(Cookie::parse(" =bar").is_err());
assert!(Cookie::parse("foo=").is_ok());
let expected = Cookie::new("foo", "bar=baz");
assert_eq_parse!("foo=bar=baz", expected);
let expected = Cookie::new("foo", "\"\"bar\"\"");
assert_eq_parse!("foo=\"\"bar\"\"", expected);
let expected = Cookie::new("foo", "\"bar");
assert_eq_parse!("foo= \"bar", expected);
assert_eq_parse!("foo=\"bar ", expected);
assert_ne_parse!("foo=\"\"bar\"", expected);
assert_ne_parse!("foo=\"\"bar \"", expected);
assert_ne_parse!("foo=\"\"bar \" ", expected);
let expected = Cookie::new("foo", "bar\"");
assert_eq_parse!("foo=bar\"", expected);
assert_ne_parse!("foo=\"bar\"\"", expected);
assert_ne_parse!("foo=\" bar\"\"", expected);
assert_ne_parse!("foo=\" bar\" \" ", expected);
let expected = Cookie::build(("foo", "bar")).partitioned(true).build();
assert_eq_parse!("foo=bar; partitioned", expected);
assert_eq_parse!("foo=bar; Partitioned", expected);
assert_eq_parse!("foo=bar; PARTITIONED", expected);
let mut expected = Cookie::new("foo", "bar");
assert_eq_parse!("foo=bar", expected);
assert_eq_parse!("foo = bar", expected);
assert_eq_parse!(" foo=bar ", expected);
assert_eq_parse!(" foo=bar ;Domain=", expected);
assert_eq_parse!(" foo=bar ;Domain= ", expected);
assert_eq_parse!(" foo=bar ;Ignored", expected);
assert_ne_parse!("foo=\"bar\"", expected);
assert_ne_parse!(" foo=\"bar \" ", expected);
let mut unexpected = Cookie::build(("foo", "bar")).http_only(false).build();
assert_ne_parse!(" foo=bar ;HttpOnly", unexpected);
assert_ne_parse!(" foo=bar; httponly", unexpected);
expected.set_http_only(true);
assert_eq_parse!(" foo=bar ;HttpOnly", expected);
assert_eq_parse!(" foo=bar ;httponly", expected);
assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected);
assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected);
expected.set_secure(true);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected);
unexpected.set_http_only(true);
unexpected.set_secure(true);
assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected);
unexpected.set_secure(false);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
expected.set_max_age(Duration::ZERO);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected);
expected.set_max_age(Duration::minutes(1));
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected);
expected.set_max_age(Duration::seconds(4));
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected);
unexpected.set_secure(true);
unexpected.set_max_age(Duration::minutes(1));
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected);
expected.set_path("/");
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected);
expected.set_path("/foo");
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected);
assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected);
unexpected.set_max_age(Duration::seconds(4));
unexpected.set_path("/bar");
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected);
assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected);
expected.set_domain("www.foo.com");
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=www.foo.com", expected);
expected.set_domain("foo.com");
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=FOO.COM", expected);
expected.set_domain(".foo.com");
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=.foo.com", expected);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=.FOO.COM", expected);
unexpected.set_path("/foo");
unexpected.set_domain("bar.com");
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com", unexpected);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=FOO.COM", unexpected);
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
let expires = parse_date(time_str, &super::FMT1).unwrap();
expected.set_expires(expires);
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected);
unexpected.set_domain("foo.com");
let bad_expires = parse_date(time_str, &super::FMT1).unwrap();
expected.set_expires(bad_expires);
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected);
}
#[test]
fn parse_abbreviated_years() {
let cookie_str = "foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT";
let cookie = Cookie::parse(cookie_str).unwrap();
assert_eq!(cookie.expires_datetime().unwrap().year(), 2020);
let cookie_str = "foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT";
let cookie = Cookie::parse(cookie_str).unwrap();
assert_eq!(cookie.expires_datetime().unwrap().year(), 2068);
let cookie_str = "foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT";
let cookie = Cookie::parse(cookie_str).unwrap();
assert_eq!(cookie.expires_datetime().unwrap().year(), 1969);
let cookie_str = "foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT";
let cookie = Cookie::parse(cookie_str).unwrap();
assert_eq!(cookie.expires_datetime().unwrap().year(), 1999);
let cookie_str = "foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT";
let cookie = Cookie::parse(cookie_str).unwrap();
assert_eq!(cookie.expires_datetime().unwrap().year(), 2069);
}
#[test]
fn parse_variant_date_fmts() {
let cookie_str = "foo=bar; expires=Sun, 06 Nov 1994 08:49:37 GMT";
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
let cookie_str = "foo=bar; expires=Sunday, 06-Nov-94 08:49:37 GMT";
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
let cookie_str = "foo=bar; expires=Sun Nov 6 08:49:37 1994";
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
}
#[test]
fn parse_very_large_max_ages() {
let mut expected = Cookie::build(("foo", "bar"))
.max_age(Duration::seconds(i64::max_value()))
.build();
let string = format!("foo=bar; Max-Age={}", 1u128 << 100);
assert_eq_parse!(&string, expected);
expected.set_max_age(Duration::seconds(0));
assert_eq_parse!("foo=bar; Max-Age=-129", expected);
let string = format!("foo=bar; Max-Age=-{}", 1u128 << 100);
assert_eq_parse!(&string, expected);
let string = format!("foo=bar; Max-Age=-{}", i64::max_value());
assert_eq_parse!(&string, expected);
let string = format!("foo=bar; Max-Age={}", i64::max_value());
expected.set_max_age(Duration::seconds(i64::max_value()));
assert_eq_parse!(&string, expected);
}
#[test]
fn odd_characters() {
let expected = Cookie::new("foo", "b%2Fr");
assert_eq_parse!("foo=b%2Fr", expected);
}
#[test]
#[cfg(feature = "percent-encode")]
fn odd_characters_encoded() {
let expected = Cookie::new("foo", "b/r");
let cookie = match Cookie::parse_encoded("foo=b%2Fr") {
Ok(cookie) => cookie,
Err(e) => panic!("Failed to parse: {:?}", e)
};
assert_eq!(cookie, expected);
}
#[test]
fn do_not_panic_on_large_max_ages() {
let max_seconds = Duration::MAX.whole_seconds();
let expected = Cookie::build(("foo", "bar"))
.max_age(Duration::seconds(max_seconds));
let too_many_seconds = (max_seconds as u64) + 1;
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected);
}
}
+367
View File
@@ -0,0 +1,367 @@
use std::marker::PhantomData;
use std::borrow::{Borrow, BorrowMut, Cow};
use crate::{CookieJar, Cookie};
/// A child jar that automatically [prefixes](Prefix) cookies.
///
/// Obtained via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`].
///
/// This jar implements the [HTTP RFC6265 draft] "cookie prefixes" extension by
/// automatically adding and removing a specified [`Prefix`] from cookies that
/// are added and retrieved from this jar, respectively. Additionally, upon
/// being added to this jar, cookies are automatically made to
/// [conform](Prefix::conform()) to the corresponding prefix's specifications.
///
/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and
/// definition are subject to change.
///
/// [HTTP RFC6265 draft]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
pub struct PrefixedJar<P: Prefix, J> {
parent: J,
_prefix: PhantomData<fn() -> P>,
}
/// The [`"__Host-"`] cookie [`Prefix`].
///
/// See [`Prefix`] and [`PrefixedJar`] for usage details.
///
/// [`"__Host-"`]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
pub struct Host;
/// The [`"__Secure-"`] cookie [`Prefix`].
///
/// See [`Prefix`] and [`PrefixedJar`] for usage details.
///
/// [`"__Secure-"`]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
pub struct Secure;
/// Trait identifying [HTTP RFC6265 draft] cookie prefixes.
///
/// A [`Prefix`] can be applied to cookies via a child [`PrefixedJar`], itself
/// obtainable via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`].
/// Cookies added/retrieved to/from these child jars have the corresponding
/// [prefix](Prefix::conform()) automatically prepended/removed as needed.
/// Additionally, added cookies are automatically make to
/// [conform](Prefix::conform()).
///
/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and
/// definition are subject to change.
///
/// [HTTP RFC6265 draft]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
pub trait Prefix: private::Sealed {
/// The prefix string to prepend.
///
/// See [`Host::PREFIX`] and [`Secure::PREFIX`] for specifics.
const PREFIX: &'static str;
/// Alias to [`Host`].
#[allow(non_upper_case_globals)]
const Host: Host = Host;
/// Alias to [`Secure`].
#[allow(non_upper_case_globals)]
const Secure: Secure = Secure;
/// Modify `cookie` so it conforms to the requirements of `self`.
///
/// See [`Host::conform()`] and [`Secure::conform()`] for specifics.
//
// This is the only required method. Everything else is shared across
// implementations via the default implementations below and should not be
// implemented.
fn conform(cookie: Cookie<'_>) -> Cookie<'_>;
/// Returns a string with `name` prefixed with `self`.
#[doc(hidden)]
#[inline(always)]
fn prefixed_name(name: &str) -> String {
format!("{}{}", Self::PREFIX, name)
}
/// Prefix `cookie`'s name with `Self`.
#[doc(hidden)]
fn prefix(mut cookie: Cookie<'_>) -> Cookie<'_> {
use crate::CookieStr;
cookie.name = CookieStr::Concrete(match cookie.name {
CookieStr::Concrete(Cow::Owned(mut string)) => {
string.insert_str(0, Self::PREFIX);
string.into()
}
_ => Self::prefixed_name(cookie.name()).into(),
});
cookie
}
/// Remove the prefix `Self` from `cookie`'s name and return it.
///
/// If the prefix isn't in `cookie`, the cookie is returned unmodified. This
/// method is expected to be called only when `cookie`'s name is known to
/// contain the prefix.
#[doc(hidden)]
fn clip(mut cookie: Cookie<'_>) -> Cookie<'_> {
use std::borrow::Cow::*;
use crate::CookieStr::*;
if !cookie.name().starts_with(Self::PREFIX) {
return cookie;
}
let len = Self::PREFIX.len();
cookie.name = match cookie.name {
Indexed(i, j) => Indexed(i + len, j),
Concrete(Borrowed(v)) => Concrete(Borrowed(&v[len..])),
Concrete(Owned(v)) => Concrete(Owned(v[len..].to_string())),
};
cookie
}
/// Prefix and _conform_ `cookie`: prefix `cookie` with `Self` and make it
/// conform to the required specification by modifying it.
#[inline]
#[doc(hidden)]
fn apply(cookie: Cookie<'_>) -> Cookie<'_> {
Self::conform(Self::prefix(cookie))
}
}
impl<P: Prefix, J> PrefixedJar<P, J> {
#[inline(always)]
pub(crate) fn new(parent: J) -> Self {
Self { parent, _prefix: PhantomData }
}
}
impl<P: Prefix, J: Borrow<CookieJar>> PrefixedJar<P, J> {
/// Fetches the `Cookie` inside this jar with the prefix `P` and removes the
/// prefix before returning it. If the cookie isn't found, returns `None`.
///
/// See [`CookieJar::prefixed()`] for more examples.
///
/// # Example
///
/// ```rust
/// use cookie::CookieJar;
/// use cookie::prefix::{Host, Secure};
///
/// // Add a `Host` prefixed cookie.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Host).add(("h0st", "value"));
/// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st");
/// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value");
/// ```
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
self.parent.borrow()
.get(&P::prefixed_name(name))
.map(|c| P::clip(c.clone()))
}
}
impl<P: Prefix, J: BorrowMut<CookieJar>> PrefixedJar<P, J> {
/// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`,
/// and the cookie's attributes are made to [`conform`](Prefix::conform()).
///
/// See [`CookieJar::prefixed_mut()`] for more examples.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
/// use cookie::prefix::{Host, Secure};
///
/// // Add a `Host` prefixed cookie.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Secure).add(Cookie::build(("name", "value")).secure(false));
/// assert_eq!(jar.prefixed(Secure).get("name").unwrap().value(), "value");
/// assert_eq!(jar.prefixed(Secure).get("name").unwrap().secure(), Some(true));
/// ```
pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().add(P::apply(cookie.into()));
}
/// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`,
/// and the cookie's attributes are made to [`conform`](Prefix::conform()).
///
/// Adding an original cookie does not affect the [`CookieJar::delta()`]
/// computation. This method is intended to be used to seed the cookie jar
/// with cookies. For accurate `delta` computations, this method should not
/// be called after calling `remove`.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
/// use cookie::prefix::{Host, Secure};
///
/// // Add a `Host` prefixed cookie.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Secure).add_original(("name", "value"));
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().add_original(P::apply(cookie.into()));
}
/// Removes `cookie` from the parent jar.
///
/// The cookie's name is prefixed with `P`, and the cookie's attributes are
/// made to [`conform`](Prefix::conform()) before attempting to remove the
/// cookie. For correct removal, the passed in `cookie` must contain the
/// same `path` and `domain` as the cookie that was initially set.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar};
/// use cookie::prefix::{Host, Secure};
///
/// let mut jar = CookieJar::new();
/// let mut prefixed_jar = jar.prefixed_mut(Host);
///
/// prefixed_jar.add(("name", "value"));
/// assert!(prefixed_jar.get("name").is_some());
///
/// prefixed_jar.remove("name");
/// assert!(prefixed_jar.get("name").is_none());
/// ```
pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().remove(P::apply(cookie.into()));
}
}
impl Prefix for Host {
/// The [`"__Host-"` prefix] string.
///
/// [`"__Host-"` prefix]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
const PREFIX: &'static str = "__Host-";
/// Modify `cookie` so it conforms to the prefix's requirements.
///
/// **Note: this method is called automatically by [`PrefixedJar`]. It _does
/// not need to_ and _should not_ be called manually under normal
/// circumstances.**
///
/// According to [RFC 6265bis-12 §4.1.3.2]:
///
/// ```text
/// If a cookie's name begins with a case-sensitive match for the string
/// __Host-, then the cookie will have been set with a Secure attribute,
/// a Path attribute with a value of /, and no Domain attribute.
/// ```
///
/// As such, to make a cookie conforn, this method:
///
/// * Sets [`secure`](Cookie::set_secure()) to `true`.
/// * Sets the [`path`](Cookie::set_path()) to `"/"`.
/// * Removes the [`domain`](Cookie::unset_domain()), if any.
///
/// [RFC 6265bis-12 §4.1.3.2]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, prefix::Host};
///
/// // A cookie with some non-conformant properties.
/// let cookie = Cookie::build(("name", "some-value"))
/// .secure(false)
/// .path("/foo/bar")
/// .domain("rocket.rs")
/// .http_only(true);
///
/// // Add the cookie to the jar.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Host).add(cookie);
///
/// // Fetch the cookie: notice it's been made to conform.
/// let cookie = jar.prefixed(Host).get("name").unwrap();
/// assert_eq!(cookie.name(), "name");
/// assert_eq!(cookie.value(), "some-value");
/// assert_eq!(cookie.secure(), Some(true));
/// assert_eq!(cookie.path(), Some("/"));
/// assert_eq!(cookie.domain(), None);
/// assert_eq!(cookie.http_only(), Some(true));
/// ```
fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> {
cookie.set_secure(true);
cookie.set_path("/");
cookie.unset_domain();
cookie
}
}
impl Prefix for Secure {
/// The [`"__Secure-"` prefix] string.
///
/// [`"__Secure-"` prefix]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
const PREFIX: &'static str = "__Secure-";
/// Modify `cookie` so it conforms to the prefix's requirements.
///
/// **Note: this method is called automatically by [`PrefixedJar`]. It _does
/// not need to_ and _should not_ be called manually under normal
/// circumstances.**
///
/// According to [RFC 6265bis-12 §4.1.3.1]:
///
/// ```text
/// If a cookie's name begins with a case-sensitive match for the string
/// __Secure-, then the cookie will have been set with a Secure
/// attribute.
/// ```
///
/// As such, to make a cookie conforn, this method:
///
/// * Sets [`secure`](Cookie::set_secure()) to `true`.
///
/// [RFC 6265bis-12 §4.1.3.1]:
/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, prefix::Secure};
///
/// // A cookie with some non-conformant properties.
/// let cookie = Cookie::build(("name", "some-value"))
/// .secure(false)
/// .path("/guide")
/// .domain("rocket.rs")
/// .http_only(true);
///
/// // Add the cookie to the jar.
/// let mut jar = CookieJar::new();
/// jar.prefixed_mut(Secure).add(cookie);
///
/// // Fetch the cookie: notice it's been made to conform.
/// let cookie = jar.prefixed(Secure).get("name").unwrap();
/// assert_eq!(cookie.name(), "name");
/// assert_eq!(cookie.value(), "some-value");
/// assert_eq!(cookie.secure(), Some(true));
/// assert_eq!(cookie.path(), Some("/guide"));
/// assert_eq!(cookie.domain(), Some("rocket.rs"));
/// assert_eq!(cookie.http_only(), Some(true));
/// ```
fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> {
cookie.set_secure(true);
cookie
}
}
mod private {
pub trait Sealed {}
impl Sealed for super::Host {}
impl Sealed for super::Secure {}
}
+108
View File
@@ -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"),
}
}
}
+314
View File
@@ -0,0 +1,314 @@
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 std::fmt::Debug for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Key").finish()
}
}
impl Key {
// An empty key structure, to be filled.
const fn zero() -> Self {
Key([0; COMBINED_KEY_LENGTH])
}
/// Creates a new `Key` from a 512-bit cryptographically random string.
///
/// The supplied key must be at least 512-bits (64 bytes). For security, the
/// master key _must_ be cryptographically random.
///
/// # Panics
///
/// Panics if `key` is less than 64 bytes in length.
///
/// For a non-panicking version, use [`Key::try_from()`] or generate a key with
/// [`Key::generate()`] or [`Key::try_generate()`].
///
/// # Example
///
/// ```rust
/// use cookie::Key;
///
/// # /*
/// let key = { /* a cryptographically random key >= 64 bytes */ };
/// # */
/// # let key: &Vec<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());
}
#[test]
fn debug_does_not_leak_key() {
let key = Key::generate();
assert_eq!(format!("{:?}", key), "Key");
}
}
+49
View File
@@ -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("another");
assert_eq!($clear.iter().count(), 1);
$secure.remove("name");
assert_eq!($clear.iter().count(), 0);
})
}
#[cfg(test)]
macro_rules! assert_secure_behaviour {
($clear:expr, $secure:expr) => ({
$secure.add(Cookie::new("secure", "secure"));
assert!($clear.get("secure").unwrap().value() != "secure");
assert!($secure.get("secure").unwrap().value() == "secure");
let mut cookie = $clear.get("secure").unwrap().clone();
let new_val = format!("{}l", cookie.value());
cookie.set_value(new_val);
$clear.add(cookie);
assert!($secure.get("secure").is_none());
let mut cookie = $clear.get("secure").unwrap().clone();
cookie.set_value("foobar");
$clear.add(cookie);
assert!($secure.get("secure").is_none());
})
}
// This is courtesty of `static_assertions`. That library is Copyright (c) 2017
// Nikolai Vazquez. See https://github.com/nvzqz/static-assertions-rs for more.
macro_rules! const_assert {
($x:expr $(,)?) => {
#[allow(unknown_lints, clippy::eq_op)]
const _: [(); 0 - !{ const ASSERT: bool = $x; ASSERT } as usize] = [];
};
}
+27
View File
@@ -0,0 +1,27 @@
extern crate rand;
mod base64 {
use base64::{DecodeError, Engine, prelude::BASE64_STANDARD};
/// Encode `input` as the standard base64 with padding.
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
BASE64_STANDARD.encode(input)
}
/// Decode `input` as the standard base64 with padding.
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
BASE64_STANDARD.decode(input)
}
}
#[macro_use]
mod macros;
mod key;
pub use self::key::*;
#[cfg(feature = "private")] mod private;
#[cfg(feature = "private")] pub use self::private::*;
#[cfg(feature = "signed")] mod signed;
#[cfg(feature = "signed")] pub use self::signed::*;
+266
View File
@@ -0,0 +1,266 @@
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<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.encrypt_cookie(&mut cookie);
self.parent.borrow_mut().add(cookie);
}
/// Adds an "original" `cookie` to parent jar. The cookie's value is
/// encrypted with authenticated encryption assuring confidentiality,
/// integrity, and authenticity. Adding an original cookie does not affect
/// the [`CookieJar::delta()`] computation. This method is intended to be
/// used to seed the cookie jar with cookies received from a client's HTTP
/// message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.private_mut(&key).add_original(Cookie::new("name", "value"));
///
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.encrypt_cookie(&mut cookie);
self.parent.borrow_mut().add_original(cookie);
}
/// Removes `cookie` from the parent jar.
///
/// For correct removal, the passed in `cookie` must contain the same `path`
/// and `domain` as the cookie that was initially set.
///
/// This is identical to [`CookieJar::remove()`]. See the method's
/// documentation for more details.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut private_jar = jar.private_mut(&key);
///
/// private_jar.add(("name", "value"));
/// assert!(private_jar.get("name").is_some());
///
/// private_jar.remove("name");
/// assert!(private_jar.get("name").is_none());
/// ```
pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().remove(cookie);
}
}
#[cfg(test)]
mod test {
use crate::{CookieJar, Cookie, Key};
#[test]
fn simple() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_simple_behaviour!(jar, jar.private_mut(&key));
}
#[test]
fn secure() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_secure_behaviour!(jar, jar.private_mut(&key));
}
#[test]
fn roundtrip() {
// Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
10, 180, 170, 187, 89, 252, 137, 110, 107]);
let mut jar = CookieJar::new();
jar.add(Cookie::new("encrypted_with_ring014",
"lObeZJorGVyeSWUA8khTO/8UCzFVBY9g0MGU6/J3NN1R5x11dn2JIA=="));
jar.add(Cookie::new("encrypted_with_ring016",
"SU1ujceILyMBg3fReqRmA9HUtAIoSPZceOM/CUpObROHEujXIjonkA=="));
let private = jar.private(&key);
assert_eq!(private.get("encrypted_with_ring014").unwrap().value(), "Tamper-proof");
assert_eq!(private.get("encrypted_with_ring016").unwrap().value(), "Tamper-proof");
}
}
+253
View File
@@ -0,0 +1,253 @@
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(("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(("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<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.sign_cookie(&mut cookie);
self.parent.borrow_mut().add(cookie);
}
/// Adds an "original" `cookie` to this jar. The cookie's value is signed
/// assuring integrity and authenticity. Adding an original cookie does not
/// affect the [`CookieJar::delta()`] computation. This method is intended
/// to be used to seed the cookie jar with cookies received from a client's
/// HTTP message.
///
/// For accurate `delta` computations, this method should not be called
/// after calling `remove`.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// jar.signed_mut(&key).add_original(("name", "value"));
///
/// assert_eq!(jar.iter().count(), 1);
/// assert_eq!(jar.delta().count(), 0);
/// ```
pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
let mut cookie = cookie.into();
self.sign_cookie(&mut cookie);
self.parent.borrow_mut().add_original(cookie);
}
/// Removes `cookie` from the parent jar.
///
/// For correct removal, the passed in `cookie` must contain the same `path`
/// and `domain` as the cookie that was initially set.
///
/// This is identical to [`CookieJar::remove()`]. See the method's
/// documentation for more details.
///
/// # Example
///
/// ```rust
/// use cookie::{CookieJar, Cookie, Key};
///
/// let key = Key::generate();
/// let mut jar = CookieJar::new();
/// let mut signed_jar = jar.signed_mut(&key);
///
/// signed_jar.add(("name", "value"));
/// assert!(signed_jar.get("name").is_some());
///
/// signed_jar.remove("name");
/// assert!(signed_jar.get("name").is_none());
/// ```
pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
self.parent.borrow_mut().remove(cookie.into());
}
}
#[cfg(test)]
mod test {
use crate::{CookieJar, Cookie, Key};
#[test]
fn simple() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_simple_behaviour!(jar, jar.signed_mut(&key));
}
#[test]
fn private() {
let key = Key::generate();
let mut jar = CookieJar::new();
assert_secure_behaviour!(jar, jar.signed_mut(&key));
}
#[test]
fn roundtrip() {
// Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
10, 180, 170, 187, 89, 252, 137, 110, 107]);
let mut jar = CookieJar::new();
jar.add(Cookie::new("signed_with_ring014",
"3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
jar.add(Cookie::new("signed_with_ring016",
"3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
let signed = jar.signed(&key);
assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
}
#[test]
fn issue_178() {
let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£";
let c = Cookie::parse(data).expect("failed to parse cookie");
let key = Key::from(&[0u8; 64]);
let mut jar = CookieJar::new();
let signed = jar.signed_mut(&key);
assert!(signed.verify(c).is_none());
}
}