cookie_store-0.21.1/.cargo_vcs_info.json0000644000000001360000000000100136020ustar { "git": { "sha1": "01d9a5f16b84019bee6bcf876f10e52063faa040" }, "path_in_vcs": "" }cookie_store-0.21.1/.github/workflows/ci.yml000064400000000000000000000041731046102023000171120ustar 00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: ci-pass: name: CI is green runs-on: ubuntu-latest needs: - msrv - build - test steps: - run: exit 0 msrv: name: MSRV runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Get MSRV package metadata id: metadata run: cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' >> $GITHUB_OUTPUT - name: Install rust (${{ steps.metadata.outputs.msrv }}) uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.metadata.outputs.msrv }} - name: Pin time and idna_adapter versions run: | cargo update cargo update -p time --precise 0.3.20 cargo update -p idna_adapter --precise 1.1.0 - uses: Swatinem/rust-cache@v2 - name: Check run: cargo check build: name: ${{ matrix.name }} runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: matrix: name: - linux / stable include: - name: linux / stable steps: - name: Checkout uses: actions/checkout@v3 - name: Install rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust || 'stable' }} profile: minimal override: true - name: Build uses: actions-rs/cargo@v1 with: command: build args: --verbose --all-features test: name: ${{ matrix.name }} runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: matrix: name: - linux / stable include: - name: linux / stable steps: - name: Checkout uses: actions/checkout@v3 - name: Install rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust || 'stable' }} profile: minimal override: true - name: Run tests uses: actions-rs/cargo@v1 with: command: test args: --verbose --all-features cookie_store-0.21.1/.gitignore000064400000000000000000000000361046102023000143610ustar 00000000000000/target **/*.rs.bk Cargo.lock cookie_store-0.21.1/CHANGELOG.md000064400000000000000000000132611046102023000142060ustar 00000000000000# Changelog ## [0.21.1] - 2024-11-09 ### Documentation - Update CONTRIBUTORS.md - Switch to using `document-feature` for genearating feature flag documentation - Improve documentation around features - Add documentation around legacy serialization vs. `serde` module ### Features - Gate serialization behind features `serde{,_json,_ron}` ### Miscellaneous Tasks - Bump `indexmap` to `2.6.0` ### Build - Set `rust-version=1.63.0` - Add `serde_json` as a default feature - Specify feature dependencies with explcit `dep:` ### Ci - Split ci check step `build` into `build` + `test`. Add `msrv` job ### Refact - De/serialize through simple `Vec` instead of `CookieStoreSerialized` - Collect legacy `mod cookie_store` serialization impl - Rename `mod serialization` -> `serde`; split out `json`, `ron` - Split `ron` and `json` serialization tests - Reorganize tests to respect `serde*` features - Move serialization into dedicated `mod serialization` ## [0.21.0] - 2024-02-08 ### Miscellaneous Tasks - Update CONTRIBUTORS.md ### Ci - Add missing v0.20.1 CHANGELOG entries - Rm `--topo-order` from `git-cliff` call ## [0.20.1] - 2024-02-08 ### Bug Fixes - Pub use `cookie_store::StoreAction` - Need to maintain 0.20.x series for [patch] behavior to work ### Miscellaneous Tasks - Update CONTRIBUTORS.md ## [0.20.0] - 2023-06-17 ### Features - Re-export dependency cookie - Add `CookieStore::new()` ### Styling - Rust_fmt changes ## [0.19.1] - 2023-06-17 ### Ci - Allow specification of last tag to generate CHANGELOG from - Fix git-cliff args for latest release - Allow serde and serde_derive to compile in parallel - Check tag format in release.sh ## [0.19.0] - 2022-11-05 ### Bug Fixes - Store clone of original raw cookie ### Cookie_store - Fix missing raw cookie elements ## [0.18.0] - 2022-10-25 ### Documentation - Remove old `reqwest_impl` REMOVAL notice ### Features - Make logging secure cookie values opt-in ### Miscellaneous Tasks - Dependency bumps - Update CONTRIBUTORS - Update to idna 0.3 - Do not use annotated tags in release.sh - Prepare version item for `release.sh` - Prepare to start using `git-cliff` ### Styling - Cargo fmt - Fix release.sh comments/whitespace ### Build - Expose feature `wasm-bindgen` ### Cookie_store - Derive clone for CookieStore - Add API to save all cookies ### Rename - New `save_all` methods to emphasize divergence from RFC behavior ## [0.17.0] - 2022-08-30 ### Miscellaneous Tasks - Prepare version item for `release.sh` - Prepare to start using `git-cliff` ## [0.16.1] * Export `cookie_domain::CookieDomain` as `pub` * Export `pub use cookie_expiration::CookieExpiration` * Export `pub use cookie_path::CookiePath` * Make `CookieStore::from_cookies` pub * Add methods `CookieStore::load_json_all` and `CookieStore::load_all` to allow for loading both __unexpired__ and __expired__ cookies. ## [0.16.0] * Update of dependencies in public API in `0.15.2` should have qualified as minor version bump ## [0.15.2] __YANKED__ * Upgrade dependencies ## [0.15.1] * Attach `Secure` cookies to requests for `http://localhost` and loopback IP addresses (e.g. `127.0.0.1`). This change aligns `cookie_store`'s behaviour to the behaviour of [Chromium-based browsers](https://bugs.chromium.org/p/chromium/issues/detail?id=1177877#c7) and [Firefox](https://hg.mozilla.org/integration/autoland/rev/c4d13b3ca1e2). ## [0.15.0] * deprecation in `v0.14.1` should have qualified as minor version bump * Upgrade dependencies ## [0.14.1] * Improve documentation on `CookieStore::get_request_cookies` * Introduce alternative `CookieStore::get_request_values`, mark `CookieStore::get_request_cookies` as deprecated, and suggest usage of `get_request_values` instead. ## [0.14.0] * **BREAKING** The `CookieStoreMutex` and `CookieStoreRwLock` implementation previously provided under the `reqwest_impl` feature have been migrated to a dedicated crate, `reqwest_cookie_store`, and the feature has been removed. * **BREAKING** `reqwest` is no longer a direct depdency, but rather a `dev-depedency`. Furthermore, now only the needed `reqwest` features (`cookies`) are enabled, as opposed to all default features. This is potentially a breaking change for users. * `reqwest` is no longer an optional dependency, it is now a `dev-dependency` for doctests. * Only enable the needed features for `reqwest` (@blyxxyz) * Upgrade `publisuffix` dependency to `v2` (@rushmorem) * Remove unused dev-dependencies ## [0.13.3] * Fix attributes & configuration for feature support in docs.rs ## [0.13.0] * Introduce optional feature `reqwest_impl`, providing implementations of the `reqwest::cookie::CookieStore` trait * Upgrade to `reqwest 0.11.2` * Upgrade to `env_logger 0.8` * Upgrade to `pretty_assertions 0.7` * Upgrade to `cookie 0.15` ## [0.12.0] * Upgrade to `cookie 0.14` * Upgrade to `time 0.2` ## [0.11.0] * Implement `{De,}Serialize` for `CookieStore` (@Felerius) ## [0.10.0] * introduce optional feature `preserve_order` which maintains cookies in insertion order. ## [0.9.0] * remove `try_from` dependency again now that `reqwest` minimum rust version is bumped * upgrade to `url 2.0` (@benesch) * Upgrade to `idna 0.2` ## [0.8.0] * Remove dependency on `failure` (seanmonstar) ## [0.7.0] * Revert removal of `try_from` dependency ## [0.6.0] * Upgrades to `cookies` v0.12 * Drop dependency `try_from` in lieu of `std::convert::TryFrom` (@oherrala) * Drop dependency on `serde_derive`, rely on `serde` only (@oherrala) ## [0.4.0] * Update to Rust 2018 edition ## [0.3.1] * Upgrades to `cookies` v0.11 * Minor dependency upgrades ## [0.3] * Upgrades to `reqwest` v0.9 * Replaces `error-chain` with `failure` ## [0.2] * Removes separate `ReqwestSession::ErrorKind`. Added as variant `::ErrorKind::Reqwest` instead. cookie_store-0.21.1/CONTRIBUTORS.md000064400000000000000000000004121046102023000146460ustar 00000000000000* @dcampbell24 * @joshstoik1 * @LMJian * @nickelc * @dtolnay * ian-fox * @1One1 * @lmd0 * @SabrinaJewson * @Expyron * @LukeMathWalker * @blyxxyz * @rushmorem * @koushiro * @Felerius * @oherrala * @seanmonstar * @twistedfall * @erickt * @benesch * @kpcyrd * @pfernie cookie_store-0.21.1/Cargo.toml0000644000000042250000000000100116030ustar # 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 = "2021" rust-version = "1.63.0" name = "cookie_store" version = "0.21.1" authors = ["Patrick Fernie "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Implementation of Cookie storage and retrieval" documentation = "https://docs.rs/cookie_store" readme = "README.md" keywords = [ "cookie", "jar", "store", "http", ] categories = [ "web-programming::http-client", "web-programming", ] license = "MIT OR Apache-2.0" repository = "https://github.com/pfernie/cookie_store" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [lib] name = "cookie_store" path = "src/lib.rs" [dependencies.cookie] version = "0.18.0" features = ["percent-encode"] [dependencies.document-features] version = "0.2.10" [dependencies.idna] version = "1.0" [dependencies.indexmap] version = "2.6.0" optional = true [dependencies.log] version = "0.4.17" [dependencies.publicsuffix] version = "2.2.3" optional = true [dependencies.ron] version = "0.8.1" optional = true [dependencies.serde] version = "1.0.147" optional = true [dependencies.serde_derive] version = "1.0.147" optional = true [dependencies.serde_json] version = "1.0.87" optional = true [dependencies.time] version = "0.3.16" [dependencies.url] version = "2.3.1" [features] default = [ "public_suffix", "serde_json", ] log_secure_cookie_values = [] preserve_order = ["dep:indexmap"] public_suffix = ["dep:publicsuffix"] serde = [ "dep:serde", "dep:serde_derive", ] serde_json = [ "serde", "dep:serde_json", ] serde_ron = [ "serde", "dep:ron", ] wasm-bindgen = ["time/wasm-bindgen"] cookie_store-0.21.1/Cargo.toml.orig000064400000000000000000000043111046102023000152600ustar 00000000000000[package] authors = ["Patrick Fernie "] description = "Implementation of Cookie storage and retrieval" name = "cookie_store" version = "0.21.1" # managed by release.sh edition = "2021" rust-version = "1.63.0" repository = "https://github.com/pfernie/cookie_store" documentation = "https://docs.rs/cookie_store" readme = "README.md" license = "MIT OR Apache-2.0" keywords = ["cookie", "jar", "store", "http"] # free text categories = ["web-programming::http-client", "web-programming"] # https://crates.io/category_slugs [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] default = ["public_suffix", "serde_json"] ## uses `indexmap::IndexMap` in lieu of HashMap internally, so cookies are maintained in insertion/creation order preserve_order = ["dep:indexmap"] ## Add support for public suffix lists, as provided by [publicsuffix](https://crates.io/crates/publicsuffix). public_suffix = ["dep:publicsuffix"] ## Enables transitive feature `time/wasm-bindgen`; necessary in `wasm` contexts. wasm-bindgen = ["time/wasm-bindgen"] ## Enable logging the values of cookies marked 'secure', off by default as values may be sensitive log_secure_cookie_values = [] #! ### Serialization ## Supports generic (format-agnostic) de/serialization for a `CookieStore`. Adds dependencies `serde` and `serde_derive`. serde = ["dep:serde", "dep:serde_derive"] ## Supports de/serialization for a `CookieStore` via the JSON format. Enables feature `serde` and adds depenency `serde_json`. serde_json = ["serde", "dep:serde_json"] ## Supports de/serialization for a `CookieStore` via the RON format. Enables feature `serde` and adds depenency `ron`. serde_ron = ["serde", "dep:ron"] [dependencies] document-features = "0.2.10" idna = "1.0" log = "0.4.17" time = "0.3.16" url = "2.3.1" indexmap = { version = "2.6.0", optional = true } publicsuffix = { version = "2.2.3", optional = true } # serialization dependencies serde = { version = "1.0.147", optional = true } serde_derive = { version = "1.0.147", optional = true } serde_json = { version = "1.0.87", optional = true } ron = { version = "0.8.1", optional = true } [dependencies.cookie] features = ["percent-encode"] version = "0.18.0" cookie_store-0.21.1/LICENSE-APACHE000064400000000000000000000261351046102023000143250ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cookie_store-0.21.1/LICENSE-MIT000064400000000000000000000020411046102023000140230ustar 00000000000000MIT License Copyright (c) 2017 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cookie_store-0.21.1/README.md000064400000000000000000000021131046102023000136460ustar 00000000000000[![Build Status](https://github.com/pfernie/cookie_store/actions/workflows/ci.yml/badge.svg)](https://github.com/pfernie/cookie_store/actions/workflows/ci.yml) [![Documentation](https://docs.rs/cookie_store/badge.svg)](https://docs.rs/cookie_store) Provides an implementation for storing and retrieving `Cookie`s per the path and domain matching rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). ## Features * `preserve_order` - if enabled, iteration order of cookies will be maintained in insertion order. Pulls in an additional dependency on the [indexmap](https://crates.io/crates/indexmap) crate. ## Usage with [reqwest](https://crates.io/crates/reqwest) Please refer to the [reqwest_cookie_store](https://crates.io/crates/reqwest_cookie_store) crate, which now provides an implementation of the `reqwest::cookie::CookieStore` trait for `cookie_store::CookieStore`. ## License This project is licensed and distributed under the terms of both the MIT license and Apache License (Version 2.0). See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) cookie_store-0.21.1/cliff.toml000064400000000000000000000041101046102023000143460ustar 00000000000000# configuration file for git-cliff (0.1.0) [changelog] # changelog header header = """ # Changelog\n """ # template for the changelog body # https://tera.netlify.app/docs/#introduction body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} {% else %}\ ## [unreleased] {% endif %}\ {% for group, commits in commits | group_by(attribute="group") %} ### {{ group | upper_first }} {% for commit in commits %} - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ {% endfor %} {% endfor %}\n """ # remove the leading and trailing whitespace from the template trim = true # changelog footer footer = """ """ [git] # parse the commits based on https://www.conventionalcommits.org conventional_commits = true # filter out the commits that are not conventional filter_unconventional = true # process each line of a commit as an individual commit split_commits = false # regex for preprocessing the commit messages commit_preprocessors = [ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, ] # regex for parsing and grouping commits commit_parsers = [ { message = "^feat", group = "Features"}, { message = "^fix", group = "Bug Fixes"}, { message = "^doc", group = "Documentation"}, { message = "^perf", group = "Performance"}, { message = "^refactor", group = "Refactor"}, { message = "^style", group = "Styling"}, { message = "^test", group = "Testing"}, { message = "^chore\\(release\\): prepare for", skip = true}, { message = "^chore", group = "Miscellaneous Tasks"}, { body = ".*security", group = "Security"}, ] # filter out the commits that are not matched by commit parsers filter_commits = false # glob pattern for matching git tags tag_pattern = "v[0-9]*" # regex for skipping tags skip_tags = "v0.1.0-beta.1" # regex for ignoring tags ignore_tags = "" # sort the tags chronologically date_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" cookie_store-0.21.1/release.sh000075500000000000000000000031101046102023000143440ustar 00000000000000#!/usr/bin/env bash git_status=$(git status --porcelain) if [[ ! -z $git_status ]]; then echo -e "\e[31muncommitted state:\e[0m" git status -s echo -e "\e[31mplease commit or tidy uncommitted state before running release\e[0m" exit fi # takes the tag as an argument (e.g. v0.1.0) if [ -n "$1" ]; then if ! $(echo "${1}" | grep -q '^v[0-9]\+\.[0-9]\+\.[0-9]\+$'); then echo -e "\e[31m${1} not a version of the expected format; please use v#.#.# format\e[0m" exit fi since_flag="--unreleased" if [[ ! -z "${2}" ]]; then since_flag="${2}" fi # update the version msg="# managed by release.sh" sed "s/^version = .* $msg$/version = \"${1#v}\" $msg/" -i Cargo.toml # update the changelog git cliff --sort newest $since_flag --tag "$1" --prepend CHANGELOG.md git diff echo -e -n "\e[33mProceed? \e[0m" read -n 1 -s -p "[y/N] " proceed echo if [[ "${proceed}" != "y" ]]; then echo -e "\e[31maborting; leaving dirty state:\e[0m" git status -s exit fi git add -A git commit -m "chore(release): prepare for $1" git show # generate a changelog for the tag message export GIT_CLIFF_TEMPLATE="\ {% for group, commits in commits | group_by(attribute=\"group\") %} {{ group | upper_first }}\ {% for commit in commits %} - {% if commit.breaking %}(breaking) {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end=\"\") }})\ {% endfor %} {% endfor %}" changelog=$(git cliff --sort newest $since_flag --strip all) git tag "$1" -m "Release $1" -m "$changelog" git show -q "$1" else echo "warn: please provide a tag" fi cookie_store-0.21.1/src/cookie.rs000064400000000000000000000727371046102023000150200ustar 00000000000000use crate::cookie_domain::CookieDomain; use crate::cookie_expiration::CookieExpiration; use crate::cookie_path::CookiePath; use crate::utils::{is_http_scheme, is_secure}; use cookie::{Cookie as RawCookie, CookieBuilder as RawCookieBuilder, ParseError}; #[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; use std::borrow::Cow; use std::convert::TryFrom; use std::fmt; use std::ops::Deref; use time; use url::Url; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { /// Cookie had attribute HttpOnly but was received from a request-uri which was not an http /// scheme NonHttpScheme, /// Cookie did not specify domain but was received from non-relative-scheme request-uri from /// which host could not be determined NonRelativeScheme, /// Cookie received from a request-uri that does not domain-match DomainMismatch, /// Cookie is Expired Expired, /// `cookie::Cookie` Parse error Parse, #[cfg(feature = "public_suffix")] /// Cookie specified a public suffix domain-attribute that does not match the canonicalized /// request-uri host PublicSuffix, /// Tried to use a CookieDomain variant of `Empty` or `NotPresent` in a context requiring a Domain value UnspecifiedDomain, } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match *self { Error::NonHttpScheme => "request-uri is not an http scheme but HttpOnly attribute set", Error::NonRelativeScheme => { "request-uri is not a relative scheme; cannot determine host" } Error::DomainMismatch => "request-uri does not domain-match the cookie", Error::Expired => "attempted to utilize an Expired Cookie", Error::Parse => "unable to parse string as cookie::Cookie", #[cfg(feature = "public_suffix")] Error::PublicSuffix => "domain-attribute value is a public suffix", Error::UnspecifiedDomain => "domain-attribute is not specified", } ) } } // cookie::Cookie::parse returns Result impl From for Error { fn from(_: ParseError) -> Error { Error::Parse } } pub type CookieResult<'a> = Result, Error>; /// A cookie conforming more closely to [IETF RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Cookie<'a> { /// The parsed Set-Cookie data #[cfg_attr(feature = "serde", serde(serialize_with = "serde_raw_cookie::serialize"))] #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_raw_cookie::deserialize"))] raw_cookie: RawCookie<'a>, /// The Path attribute from a Set-Cookie header or the default-path as /// determined from /// the request-uri pub path: CookiePath, /// The Domain attribute from a Set-Cookie header, or a HostOnly variant if no /// non-empty Domain attribute /// found pub domain: CookieDomain, /// For a persistent Cookie (see [IETF RFC6265 Section /// 5.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3)), /// the expiration time as defined by the Max-Age or Expires attribute, /// otherwise SessionEnd, /// indicating a non-persistent `Cookie` that should expire at the end of the /// session pub expires: CookieExpiration, } #[cfg(feature = "serde")] mod serde_raw_cookie { use cookie::Cookie as RawCookie; use serde::de::Error; use serde::de::Unexpected; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::str::FromStr; pub fn serialize(cookie: &RawCookie<'_>, serializer: S) -> Result where S: Serializer, { cookie.to_string().serialize(serializer) } pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'a>, { let cookie = String::deserialize(deserializer)?; match RawCookie::from_str(&cookie) { Ok(cookie) => Ok(cookie), Err(_) => Err(D::Error::invalid_value( Unexpected::Str(&cookie), &"a cookie string", )), } } } impl<'a> Cookie<'a> { /// Whether this `Cookie` should be included for `request_url` pub fn matches(&self, request_url: &Url) -> bool { self.path.matches(request_url) && self.domain.matches(request_url) && (!self.raw_cookie.secure().unwrap_or(false) || is_secure(request_url)) && (!self.raw_cookie.http_only().unwrap_or(false) || is_http_scheme(request_url)) } /// Should this `Cookie` be persisted across sessions? pub fn is_persistent(&self) -> bool { match self.expires { CookieExpiration::AtUtc(_) => true, CookieExpiration::SessionEnd => false, } } /// Expire this cookie pub fn expire(&mut self) { self.expires = CookieExpiration::from(0u64); } /// Return whether the `Cookie` is expired *now* pub fn is_expired(&self) -> bool { self.expires.is_expired() } /// Indicates if the `Cookie` expires as of `utc_tm`. pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool { self.expires.expires_by(utc_tm) } /// Parses a new `cookie_store::Cookie` from `cookie_str`. pub fn parse(cookie_str: S, request_url: &Url) -> CookieResult<'a> where S: Into>, { Cookie::try_from_raw_cookie(&RawCookie::parse(cookie_str)?, request_url) } /// Create a new `cookie_store::Cookie` from a `cookie::Cookie` (from the `cookie` crate) /// received from `request_url`. pub fn try_from_raw_cookie(raw_cookie: &RawCookie<'a>, request_url: &Url) -> CookieResult<'a> { if raw_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { // If the cookie was received from a "non-HTTP" API and the // cookie's http-only-flag is set, abort these steps and ignore the // cookie entirely. return Err(Error::NonHttpScheme); } let domain = match CookieDomain::try_from(raw_cookie) { // 6. If the domain-attribute is non-empty: Ok(d @ CookieDomain::Suffix(_)) => { if !d.matches(request_url) { // If the canonicalized request-host does not domain-match the // domain-attribute: // Ignore the cookie entirely and abort these steps. Err(Error::DomainMismatch) } else { // Otherwise: // Set the cookie's host-only-flag to false. // Set the cookie's domain to the domain-attribute. Ok(d) } } Err(_) => Err(Error::Parse), // Otherwise: // Set the cookie's host-only-flag to true. // Set the cookie's domain to the canonicalized request-host. _ => CookieDomain::host_only(request_url), }?; let path = raw_cookie .path() .as_ref() .and_then(|p| CookiePath::parse(p)) .unwrap_or_else(|| CookiePath::default_path(request_url)); // per RFC6265, Max-Age takes precedence, then Expires, otherwise is Session // only let expires = if let Some(max_age) = raw_cookie.max_age() { CookieExpiration::from(max_age) } else if let Some(expiration) = raw_cookie.expires() { CookieExpiration::from(expiration) } else { CookieExpiration::SessionEnd }; Ok(Cookie { raw_cookie: raw_cookie.clone(), path, expires, domain, }) } pub fn into_owned(self) -> Cookie<'static> { Cookie { raw_cookie: self.raw_cookie.into_owned(), path: self.path, domain: self.domain, expires: self.expires, } } } impl<'a> Deref for Cookie<'a> { type Target = RawCookie<'a>; fn deref(&self) -> &Self::Target { &self.raw_cookie } } impl<'a> From> for RawCookie<'a> { fn from(cookie: Cookie<'a>) -> RawCookie<'static> { let mut builder = RawCookieBuilder::new(cookie.name().to_owned(), cookie.value().to_owned()); // Max-Age is relative, will not have same meaning now, so only set `Expires`. match cookie.expires { CookieExpiration::AtUtc(utc_tm) => { builder = builder.expires(utc_tm); } CookieExpiration::SessionEnd => {} } if cookie.path.is_from_path_attr() { builder = builder.path(String::from(cookie.path)); } if let CookieDomain::Suffix(s) = cookie.domain { builder = builder.domain(s); } builder.build() } } #[cfg(test)] mod tests { use super::Cookie; use crate::cookie_domain::CookieDomain; use crate::cookie_expiration::CookieExpiration; use cookie::Cookie as RawCookie; use time::{Duration, OffsetDateTime}; use url::Url; use crate::utils::test as test_utils; fn cmp_domain(cookie: &str, url: &str, exp: CookieDomain) { let ua = test_utils::make_cookie(cookie, url, None, None); assert!(ua.domain == exp, "\n{:?}", ua); } #[test] fn no_domain() { let url = test_utils::url("http://example.com/foo/bar"); cmp_domain( "cookie1=value1", "http://example.com/foo/bar", CookieDomain::host_only(&url).expect("unable to parse domain"), ); } // per RFC6265: // If the attribute-value is empty, the behavior is undefined. However, // the user agent SHOULD ignore the cookie-av entirely. #[test] fn empty_domain() { let url = test_utils::url("http://example.com/foo/bar"); cmp_domain( "cookie1=value1; Domain=", "http://example.com/foo/bar", CookieDomain::host_only(&url).expect("unable to parse domain"), ); } #[test] fn mismatched_domain() { let ua = Cookie::parse( "cookie1=value1; Domain=notmydomain.com", &test_utils::url("http://example.com/foo/bar"), ); assert!(ua.is_err(), "{:?}", ua); } #[test] fn domains() { fn domain_from(domain: &str, request_url: &str, is_some: bool) { let cookie_str = format!("cookie1=value1; Domain={}", domain); let raw_cookie = RawCookie::parse(cookie_str).unwrap(); let cookie = Cookie::try_from_raw_cookie(&raw_cookie, &test_utils::url(request_url)); assert_eq!(is_some, cookie.is_ok()) } // The user agent will reject cookies unless the Domain attribute // specifies a scope for the cookie that would include the origin // server. For example, the user agent will accept a cookie with a // Domain attribute of "example.com" or of "foo.example.com" from // foo.example.com, but the user agent will not accept a cookie with a // Domain attribute of "bar.example.com" or of "baz.foo.example.com". domain_from("example.com", "http://foo.example.com", true); domain_from(".example.com", "http://foo.example.com", true); domain_from("foo.example.com", "http://foo.example.com", true); domain_from(".foo.example.com", "http://foo.example.com", true); domain_from("oo.example.com", "http://foo.example.com", false); domain_from("myexample.com", "http://foo.example.com", false); domain_from("bar.example.com", "http://foo.example.com", false); domain_from("baz.foo.example.com", "http://foo.example.com", false); } #[test] fn httponly() { let c = RawCookie::parse("cookie1=value1; HttpOnly").unwrap(); let url = Url::parse("ftp://example.com/foo/bar").unwrap(); let ua = Cookie::try_from_raw_cookie(&c, &url); assert!(ua.is_err(), "{:?}", ua); } #[test] fn identical_domain() { cmp_domain( "cookie1=value1; Domain=example.com", "http://example.com/foo/bar", CookieDomain::Suffix(String::from("example.com")), ); } #[test] fn identical_domain_leading_dot() { cmp_domain( "cookie1=value1; Domain=.example.com", "http://example.com/foo/bar", CookieDomain::Suffix(String::from("example.com")), ); } #[test] fn identical_domain_two_leading_dots() { cmp_domain( "cookie1=value1; Domain=..example.com", "http://..example.com/foo/bar", CookieDomain::Suffix(String::from(".example.com")), ); } #[test] fn upper_case_domain() { cmp_domain( "cookie1=value1; Domain=EXAMPLE.com", "http://example.com/foo/bar", CookieDomain::Suffix(String::from("example.com")), ); } fn cmp_path(cookie: &str, url: &str, exp: &str) { let ua = test_utils::make_cookie(cookie, url, None, None); assert!(String::from(ua.path.clone()) == exp, "\n{:?}", ua); } #[test] fn no_path() { // no Path specified cmp_path("cookie1=value1", "http://example.com/foo/bar/", "/foo/bar"); cmp_path("cookie1=value1", "http://example.com/foo/bar", "/foo"); cmp_path("cookie1=value1", "http://example.com/foo", "/"); cmp_path("cookie1=value1", "http://example.com/", "/"); cmp_path("cookie1=value1", "http://example.com", "/"); } #[test] fn empty_path() { // Path specified with empty value cmp_path( "cookie1=value1; Path=", "http://example.com/foo/bar/", "/foo/bar", ); cmp_path( "cookie1=value1; Path=", "http://example.com/foo/bar", "/foo", ); cmp_path("cookie1=value1; Path=", "http://example.com/foo", "/"); cmp_path("cookie1=value1; Path=", "http://example.com/", "/"); cmp_path("cookie1=value1; Path=", "http://example.com", "/"); } #[test] fn invalid_path() { // Invalid Path specified (first character not /) cmp_path( "cookie1=value1; Path=baz", "http://example.com/foo/bar/", "/foo/bar", ); cmp_path( "cookie1=value1; Path=baz", "http://example.com/foo/bar", "/foo", ); cmp_path("cookie1=value1; Path=baz", "http://example.com/foo", "/"); cmp_path("cookie1=value1; Path=baz", "http://example.com/", "/"); cmp_path("cookie1=value1; Path=baz", "http://example.com", "/"); } #[test] fn path() { // Path specified, single / cmp_path( "cookie1=value1; Path=/baz", "http://example.com/foo/bar/", "/baz", ); // Path specified, multiple / (for valid attribute-value on path, take full // string) cmp_path( "cookie1=value1; Path=/baz/", "http://example.com/foo/bar/", "/baz/", ); } // expiry-related tests #[inline] fn in_days(days: i64) -> OffsetDateTime { OffsetDateTime::now_utc() + Duration::days(days) } #[inline] fn in_minutes(mins: i64) -> OffsetDateTime { OffsetDateTime::now_utc() + Duration::minutes(mins) } #[test] fn max_age_bounds() { let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", None, Some(9223372036854776), ); assert!(match ua.expires { CookieExpiration::AtUtc(_) => true, _ => false, }); } #[test] fn max_age() { let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", None, Some(60), ); assert!(!ua.is_expired()); assert!(ua.expires_by(&in_minutes(2))); } #[test] fn expired() { let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", None, Some(0u64), ); assert!(ua.is_expired()); assert!(ua.expires_by(&in_days(-1))); let ua = test_utils::make_cookie( "cookie1=value1; Max-Age=0", "http://example.com/foo/bar", None, None, ); assert!(ua.is_expired()); assert!(ua.expires_by(&in_days(-1))); let ua = test_utils::make_cookie( "cookie1=value1; Max-Age=-1", "http://example.com/foo/bar", None, None, ); assert!(ua.is_expired()); assert!(ua.expires_by(&in_days(-1))); } #[test] fn session_end() { let ua = test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None); assert!(match ua.expires { CookieExpiration::SessionEnd => true, _ => false, }); assert!(!ua.is_expired()); assert!(!ua.expires_by(&in_days(1))); assert!(!ua.expires_by(&in_days(-1))); } #[test] fn expires_tmrw_at_utc() { let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", Some(in_days(1)), None, ); assert!(!ua.is_expired()); assert!(ua.expires_by(&in_days(2))); } #[test] fn expired_yest_at_utc() { let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", Some(in_days(-1)), None, ); assert!(ua.is_expired()); assert!(!ua.expires_by(&in_days(-2))); } #[test] fn is_persistent() { let ua = test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None); assert!(!ua.is_persistent()); // SessionEnd let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", Some(in_days(1)), None, ); assert!(ua.is_persistent()); // AtUtc from Expires let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", Some(in_days(1)), Some(60), ); assert!(ua.is_persistent()); // AtUtc from Max-Age } #[test] fn max_age_overrides_expires() { // Expires indicates expiration yesterday, but Max-Age indicates expiry in 1 // minute let ua = test_utils::make_cookie( "cookie1=value1", "http://example.com/foo/bar", Some(in_days(-1)), Some(60), ); assert!(!ua.is_expired()); assert!(ua.expires_by(&in_minutes(2))); } // A request-path path-matches a given cookie-path if at least one of // the following conditions holds: // o The cookie-path and the request-path are identical. // o The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/"). // o The cookie-path is a prefix of the request-path, and the first // character of the request-path that is not included in the cookie- // path is a %x2F ("/") character. #[test] fn matches() { fn do_match(exp: bool, cookie: &str, src_url: &str, request_url: Option<&str>) { let ua = test_utils::make_cookie(cookie, src_url, None, None); let request_url = request_url.unwrap_or(src_url); assert!( exp == ua.matches(&Url::parse(request_url).unwrap()), "\n>> {:?}\nshould{}match\n>> {:?}\n", ua, if exp { " " } else { " NOT " }, request_url ); } fn is_match(cookie: &str, url: &str, request_url: Option<&str>) { do_match(true, cookie, url, request_url); } fn is_mismatch(cookie: &str, url: &str, request_url: Option<&str>) { do_match(false, cookie, url, request_url); } // match: request-path & cookie-path (defaulted from request-uri) identical is_match("cookie1=value1", "http://example.com/foo/bar", None); // mismatch: request-path & cookie-path do not match is_mismatch( "cookie1=value1", "http://example.com/bus/baz/", Some("http://example.com/foo/bar"), ); is_mismatch( "cookie1=value1; Path=/bus/baz", "http://example.com/foo/bar", None, ); // match: cookie-path a prefix of request-path and last character of // cookie-path is / is_match( "cookie1=value1", "http://example.com/foo/bar", Some("http://example.com/foo/bar"), ); is_match( "cookie1=value1; Path=/foo/", "http://example.com/foo/bar", None, ); // mismatch: cookie-path a prefix of request-path but last character of // cookie-path is not / // and first character of request-path not included in cookie-path is not / is_mismatch( "cookie1=value1", "http://example.com/fo/", Some("http://example.com/foo/bar"), ); is_mismatch( "cookie1=value1; Path=/fo", "http://example.com/foo/bar", None, ); // match: cookie-path a prefix of request-path and first character of // request-path // not included in the cookie-path is / is_match( "cookie1=value1", "http://example.com/foo/", Some("http://example.com/foo/bar"), ); is_match( "cookie1=value1; Path=/foo", "http://example.com/foo/bar", None, ); // match: Path overridden to /, which matches all paths from the domain is_match( "cookie1=value1; Path=/", "http://example.com/foo/bar", Some("http://example.com/bus/baz"), ); // mismatch: different domain is_mismatch( "cookie1=value1", "http://example.com/foo/", Some("http://notmydomain.com/foo/bar"), ); is_mismatch( "cookie1=value1; Domain=example.com", "http://foo.example.com/foo/", Some("http://notmydomain.com/foo/bar"), ); // match: secure protocol is_match( "cookie1=value1; Secure", "http://example.com/foo/bar", Some("https://example.com/foo/bar"), ); // mismatch: non-secure protocol is_mismatch( "cookie1=value1; Secure", "http://example.com/foo/bar", Some("http://example.com/foo/bar"), ); // match: no http restriction is_match( "cookie1=value1", "http://example.com/foo/bar", Some("ftp://example.com/foo/bar"), ); // match: http protocol is_match( "cookie1=value1; HttpOnly", "http://example.com/foo/bar", Some("http://example.com/foo/bar"), ); is_match( "cookie1=value1; HttpOnly", "http://example.com/foo/bar", Some("HTTP://example.com/foo/bar"), ); is_match( "cookie1=value1; HttpOnly", "http://example.com/foo/bar", Some("https://example.com/foo/bar"), ); // mismatch: http requried is_mismatch( "cookie1=value1; HttpOnly", "http://example.com/foo/bar", Some("ftp://example.com/foo/bar"), ); is_mismatch( "cookie1=value1; HttpOnly", "http://example.com/foo/bar", Some("data:nonrelativescheme"), ); } } #[cfg(all(test, feature = "serde_json"))] mod serde_json_tests { use crate::cookie::Cookie; use crate::cookie_expiration::CookieExpiration; use crate::utils::test as test_utils; use crate::utils::test::*; use serde_json::json; use time; fn encode_decode(c: &Cookie<'_>, expected: serde_json::Value) { let encoded = serde_json::to_value(c).unwrap(); assert_eq!( expected, encoded, "\nexpected: '{}'\n encoded: '{}'", expected.to_string(), encoded.to_string() ); let decoded: Cookie<'_> = serde_json::from_value(encoded).unwrap(); assert_eq!( *c, decoded, "\nexpected: '{}'\n decoded: '{}'", c.to_string(), decoded.to_string() ); } #[test] fn serde() { encode_decode( &test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None), json!({ "raw_cookie": "cookie1=value1", "path": ["/foo", false], "domain": { "HostOnly": "example.com" }, "expires": "SessionEnd" }), ); encode_decode( &test_utils::make_cookie( "cookie2=value2; Domain=example.com", "http://foo.example.com/foo/bar", None, None, ), json!({ "raw_cookie": "cookie2=value2; Domain=example.com", "path": ["/foo", false], "domain": { "Suffix": "example.com" }, "expires": "SessionEnd" }), ); encode_decode( &test_utils::make_cookie( "cookie3=value3; Path=/foo/bar", "http://foo.example.com/foo", None, None, ), json!({ "raw_cookie": "cookie3=value3; Path=/foo/bar", "path": ["/foo/bar", true], "domain": { "HostOnly": "foo.example.com" }, "expires": "SessionEnd", }), ); let at_utc = time::macros::date!(2015 - 08 - 11) .with_time(time::macros::time!(16:41:42)) .assume_utc(); encode_decode( &test_utils::make_cookie( "cookie4=value4", "http://example.com/foo/bar", Some(at_utc), None, ), json!({ "raw_cookie": "cookie4=value4; Expires=Tue, 11 Aug 2015 16:41:42 GMT", "path": ["/foo", false], "domain": { "HostOnly": "example.com" }, "expires": { "AtUtc": at_utc.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, }), ); let expires = test_utils::make_cookie( "cookie5=value5", "http://example.com/foo/bar", Some(in_minutes(10)), None, ); let utc_tm = match expires.expires { CookieExpiration::AtUtc(ref utc_tm) => utc_tm, CookieExpiration::SessionEnd => unreachable!(), }; let utc_formatted = utc_tm .format(&time::format_description::well_known::Rfc2822) .unwrap() .to_string() .replace("+0000", "GMT"); let raw_cookie_value = format!("cookie5=value5; Expires={utc_formatted}"); encode_decode( &expires, json!({ "raw_cookie": raw_cookie_value, "path":["/foo", false], "domain": { "HostOnly": "example.com" }, "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, }), ); dbg!(&at_utc); let max_age = test_utils::make_cookie( "cookie6=value6", "http://example.com/foo/bar", Some(at_utc), Some(10), ); dbg!(&max_age); let utc_tm = match max_age.expires { CookieExpiration::AtUtc(ref utc_tm) => time::OffsetDateTime::parse( &utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap(), &time::format_description::well_known::Rfc3339, ) .expect("could not re-parse time"), CookieExpiration::SessionEnd => unreachable!(), }; dbg!(&utc_tm); encode_decode( &max_age, json!({ "raw_cookie": "cookie6=value6; Max-Age=10; Expires=Tue, 11 Aug 2015 16:41:42 GMT", "path":["/foo", false], "domain": { "HostOnly": "example.com" }, "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, }), ); let max_age = test_utils::make_cookie( "cookie7=value7", "http://example.com/foo/bar", None, Some(10), ); let utc_tm = match max_age.expires { CookieExpiration::AtUtc(ref utc_tm) => utc_tm, CookieExpiration::SessionEnd => unreachable!(), }; encode_decode( &max_age, json!({ "raw_cookie": "cookie7=value7; Max-Age=10", "path":["/foo", false], "domain": { "HostOnly": "example.com" }, "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, }), ); } } cookie_store-0.21.1/src/cookie_domain.rs000064400000000000000000000371711046102023000163400ustar 00000000000000use std; use cookie::Cookie as RawCookie; use idna; #[cfg(feature = "public_suffix")] use publicsuffix::{List, Psl, Suffix}; #[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; use url::{Host, Url}; use crate::utils::is_host_name; use crate::CookieError; pub fn is_match(domain: &str, request_url: &Url) -> bool { CookieDomain::try_from(domain) .map(|domain| domain.matches(request_url)) .unwrap_or(false) } /// The domain of a `Cookie` #[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CookieDomain { /// No Domain attribute in Set-Cookie header HostOnly(String), /// Domain attribute from Set-Cookie header Suffix(String), /// Domain attribute was not present in the Set-Cookie header NotPresent, /// Domain attribute-value was empty; technically undefined behavior, but suggested that this /// be treated as invalid Empty, } // 5.1.3. Domain Matching // A string domain-matches a given domain string if at least one of the // following conditions hold: // // o The domain string and the string are identical. (Note that both // the domain string and the string will have been canonicalized to // lower case at this point.) // // o All of the following conditions hold: // // * The domain string is a suffix of the string. // // * The last character of the string that is not included in the // domain string is a %x2E (".") character. // // * The string is a host name (i.e., not an IP address). /// The concept of a domain match per [IETF RFC6265 Section /// 5.1.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3) impl CookieDomain { /// Get the CookieDomain::HostOnly variant based on `request_url`. This is the effective behavior of /// setting the domain-attribute to empty pub fn host_only(request_url: &Url) -> Result { request_url .host() .ok_or(CookieError::NonRelativeScheme) .map(|h| match h { Host::Domain(d) => CookieDomain::HostOnly(d.into()), Host::Ipv4(addr) => CookieDomain::HostOnly(format!("{}", addr)), Host::Ipv6(addr) => CookieDomain::HostOnly(format!("[{}]", addr)), }) } /// Tests if the given `url::Url` meets the domain-match criteria pub fn matches(&self, request_url: &Url) -> bool { if let Some(url_host) = request_url.host_str() { match *self { CookieDomain::HostOnly(ref host) => host == url_host, CookieDomain::Suffix(ref suffix) => { suffix == url_host || (is_host_name(url_host) && url_host.ends_with(suffix) && url_host[(url_host.len() - suffix.len() - 1)..].starts_with('.')) } CookieDomain::NotPresent | CookieDomain::Empty => false, // nothing can match the Empty case } } else { false // not a matchable scheme } } /// Tests if the given `url::Url` has a request-host identical to the domain attribute pub fn host_is_identical(&self, request_url: &Url) -> bool { if let Some(url_host) = request_url.host_str() { match *self { CookieDomain::HostOnly(ref host) => host == url_host, CookieDomain::Suffix(ref suffix) => suffix == url_host, CookieDomain::NotPresent | CookieDomain::Empty => false, // nothing can match the Empty case } } else { false // not a matchable scheme } } /// Tests if the domain-attribute is a public suffix as indicated by the provided /// `publicsuffix::List`. #[cfg(feature = "public_suffix")] pub fn is_public_suffix(&self, psl: &List) -> bool { if let Some(domain) = self.as_cow().as_ref().map(|d| d.as_bytes()) { psl.suffix(domain) // Only consider suffixes explicitly listed in the public suffix list // to avoid issues like https://github.com/curl/curl/issues/658 .filter(Suffix::is_known) .filter(|suffix| suffix == &domain) .is_some() } else { false } } /// Get a borrowed string representation of the domain. For `Empty` and `NotPresent` variants, /// `None` shall be returned; pub fn as_cow(&self) -> Option> { match *self { CookieDomain::HostOnly(ref s) | CookieDomain::Suffix(ref s) => { Some(std::borrow::Cow::Borrowed(s)) } CookieDomain::Empty | CookieDomain::NotPresent => None, } } } /// Construct a `CookieDomain::Suffix` from a string, stripping a single leading '.' if present. /// If the source string is empty, returns the `CookieDomain::Empty` variant. impl<'a> TryFrom<&'a str> for CookieDomain { type Error = crate::Error; fn try_from(value: &str) -> Result { idna::domain_to_ascii(value.trim()) .map_err(super::IdnaErrors::from) .map_err(Into::into) .map(|domain| { if domain.is_empty() || "." == domain { CookieDomain::Empty } else if domain.starts_with('.') { CookieDomain::Suffix(String::from(&domain[1..])) } else { CookieDomain::Suffix(domain) } }) } } /// Construct a `CookieDomain::Suffix` from a `cookie::Cookie`, which handles stripping a leading /// '.' for us. If the cookie.domain is None or an empty string, the `CookieDomain::Empty` variant /// is returned. /// __NOTE__: `cookie::Cookie` domain values already have the leading '.' stripped. To avoid /// performing this step twice, the `From<&cookie::Cookie>` impl should be used, /// instead of passing `cookie.domain` to the `From<&str>` impl. impl<'a, 'c> TryFrom<&'a RawCookie<'c>> for CookieDomain { type Error = crate::Error; fn try_from(cookie: &'a RawCookie<'c>) -> Result { if let Some(domain) = cookie.domain() { idna::domain_to_ascii(domain.trim()) .map_err(super::IdnaErrors::from) .map_err(Into::into) .map(|domain| { if domain.is_empty() { CookieDomain::Empty } else { CookieDomain::Suffix(domain) } }) } else { Ok(CookieDomain::NotPresent) } } } impl<'a> From<&'a CookieDomain> for String { fn from(c: &'a CookieDomain) -> String { match *c { CookieDomain::HostOnly(ref h) => h.to_owned(), CookieDomain::Suffix(ref s) => s.to_owned(), CookieDomain::Empty | CookieDomain::NotPresent => "".to_owned(), } } } #[cfg(test)] mod tests { use cookie::Cookie as RawCookie; use std::convert::TryFrom; use url::Url; use super::CookieDomain; use crate::utils::test::*; #[inline] fn matches(expected: bool, cookie_domain: &CookieDomain, url: &str) { let url = Url::parse(url).unwrap(); assert!( expected == cookie_domain.matches(&url), "cookie_domain: {:?} url: {:?}, url.host_str(): {:?}", cookie_domain, url, url.host_str() ); } #[inline] fn variants(expected: bool, cookie_domain: &CookieDomain, url: &str) { matches(expected, cookie_domain, url); matches(expected, cookie_domain, &format!("{}/", url)); matches(expected, cookie_domain, &format!("{}:8080", url)); matches(expected, cookie_domain, &format!("{}/foo/bar", url)); matches(expected, cookie_domain, &format!("{}:8080/foo/bar", url)); } #[test] fn matches_hostonly() { { let url = url("http://example.com"); // HostOnly must be an identical string match, and may be an IP address // or a hostname let host_name = CookieDomain::host_only(&url).expect("unable to parse domain"); matches(false, &host_name, "data:nonrelative"); variants(true, &host_name, "http://example.com"); variants(false, &host_name, "http://example.org"); // per RFC6265: // WARNING: Some existing user agents treat an absent Domain // attribute as if the Domain attribute were present and contained // the current host name. For example, if example.com returns a Set- // Cookie header without a Domain attribute, these user agents will // erroneously send the cookie to www.example.com as well. variants(false, &host_name, "http://foo.example.com"); variants(false, &host_name, "http://127.0.0.1"); variants(false, &host_name, "http://[::1]"); } { let url = url("http://127.0.0.1"); let ip4 = CookieDomain::host_only(&url).expect("unable to parse Ipv4"); matches(false, &ip4, "data:nonrelative"); variants(true, &ip4, "http://127.0.0.1"); variants(false, &ip4, "http://[::1]"); } { let url = url("http://[::1]"); let ip6 = CookieDomain::host_only(&url).expect("unable to parse Ipv6"); matches(false, &ip6, "data:nonrelative"); variants(false, &ip6, "http://127.0.0.1"); variants(true, &ip6, "http://[::1]"); } } #[test] fn from_strs() { assert_eq!( CookieDomain::Empty, CookieDomain::try_from("").expect("unable to parse domain") ); assert_eq!( CookieDomain::Empty, CookieDomain::try_from(".").expect("unable to parse domain") ); // per [IETF RFC6265 Section 5.2.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3) //If the first character of the attribute-value string is %x2E ("."): // //Let cookie-domain be the attribute-value without the leading %x2E //(".") character. assert_eq!( CookieDomain::Suffix(String::from(".")), CookieDomain::try_from("..").expect("unable to parse domain") ); assert_eq!( CookieDomain::Suffix(String::from("example.com")), CookieDomain::try_from("example.com").expect("unable to parse domain") ); assert_eq!( CookieDomain::Suffix(String::from("example.com")), CookieDomain::try_from(".example.com").expect("unable to parse domain") ); assert_eq!( CookieDomain::Suffix(String::from(".example.com")), CookieDomain::try_from("..example.com").expect("unable to parse domain") ); } #[test] fn from_raw_cookie() { fn raw_cookie(s: &str) -> RawCookie<'_> { RawCookie::parse(s).unwrap() } assert_eq!( CookieDomain::NotPresent, CookieDomain::try_from(&raw_cookie("cookie=value")).expect("unable to parse domain") ); // cookie::Cookie handles this (cookie.domain == None) assert_eq!( CookieDomain::NotPresent, CookieDomain::try_from(&raw_cookie("cookie=value; Domain=")) .expect("unable to parse domain") ); // cookie::Cookie does not handle this (empty after stripping leading dot) assert_eq!( CookieDomain::Empty, CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.")) .expect("unable to parse domain") ); assert_eq!( CookieDomain::Suffix(String::from("example.com")), CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.example.com")) .expect("unable to parse domain") ); assert_eq!( CookieDomain::Suffix(String::from("example.com")), CookieDomain::try_from(&raw_cookie("cookie=value; Domain=example.com")) .expect("unable to parse domain") ); } #[test] fn matches_suffix() { { let suffix = CookieDomain::try_from("example.com").expect("unable to parse domain"); variants(true, &suffix, "http://example.com"); // exact match variants(true, &suffix, "http://foo.example.com"); // suffix match variants(false, &suffix, "http://example.org"); // no match variants(false, &suffix, "http://xample.com"); // request is the suffix, no match variants(false, &suffix, "http://fooexample.com"); // suffix, but no "." b/w foo and example, no match } { // strip leading dot let suffix = CookieDomain::try_from(".example.com").expect("unable to parse domain"); variants(true, &suffix, "http://example.com"); variants(true, &suffix, "http://foo.example.com"); variants(false, &suffix, "http://example.org"); variants(false, &suffix, "http://xample.com"); variants(false, &suffix, "http://fooexample.com"); } { // only first leading dot is stripped let suffix = CookieDomain::try_from("..example.com").expect("unable to parse domain"); variants(true, &suffix, "http://.example.com"); variants(true, &suffix, "http://foo..example.com"); variants(false, &suffix, "http://example.com"); variants(false, &suffix, "http://foo.example.com"); variants(false, &suffix, "http://example.org"); variants(false, &suffix, "http://xample.com"); variants(false, &suffix, "http://fooexample.com"); } { // an exact string match, although an IP is specified let suffix = CookieDomain::try_from("127.0.0.1").expect("unable to parse Ipv4"); variants(true, &suffix, "http://127.0.0.1"); } { // an exact string match, although an IP is specified let suffix = CookieDomain::try_from("[::1]").expect("unable to parse Ipv6"); variants(true, &suffix, "http://[::1]"); } { // non-identical suffix match only works for host names (i.e. not IPs) let suffix = CookieDomain::try_from("0.0.1").expect("unable to parse Ipv4"); variants(false, &suffix, "http://127.0.0.1"); } } } #[cfg(all(test, feature = "serde_json"))] mod serde_json_tests { use serde_json; use std::convert::TryFrom; use crate::cookie_domain::CookieDomain; use crate::utils::test::*; fn encode_decode(cd: &CookieDomain, exp_json: &str) { let encoded = serde_json::to_string(cd).unwrap(); assert!( exp_json == encoded, "expected: '{}'\n encoded: '{}'", exp_json, encoded ); let decoded: CookieDomain = serde_json::from_str(&encoded).unwrap(); assert!( *cd == decoded, "expected: '{:?}'\n decoded: '{:?}'", cd, decoded ); } #[test] fn serde() { let url = url("http://example.com"); encode_decode( &CookieDomain::host_only(&url).expect("cannot parse domain"), "{\"HostOnly\":\"example.com\"}", ); encode_decode( &CookieDomain::try_from(".example.com").expect("cannot parse domain"), "{\"Suffix\":\"example.com\"}", ); encode_decode(&CookieDomain::NotPresent, "\"NotPresent\""); encode_decode(&CookieDomain::Empty, "\"Empty\""); } } cookie_store-0.21.1/src/cookie_expiration.rs000064400000000000000000000116301046102023000172430ustar 00000000000000use std; #[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; use time::{self, OffsetDateTime}; /// When a given `Cookie` expires #[derive(Eq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum CookieExpiration { /// `Cookie` expires at the given UTC time, as set from either the Max-Age /// or Expires attribute of a Set-Cookie header #[cfg_attr(feature = "serde", serde(with = "crate::rfc3339_fmt"))] AtUtc(OffsetDateTime), /// `Cookie` expires at the end of the current `Session`; this means the cookie /// is not persistent SessionEnd, } // We directly impl `PartialEq` as the cookie Expires attribute does not include nanosecond precision impl std::cmp::PartialEq for CookieExpiration { fn eq(&self, other: &Self) -> bool { match (self, other) { (CookieExpiration::SessionEnd, CookieExpiration::SessionEnd) => true, (CookieExpiration::AtUtc(this_offset), CookieExpiration::AtUtc(other_offset)) => { // All instances should already be UTC offset this_offset.date() == other_offset.date() && this_offset.time().hour() == other_offset.time().hour() && this_offset.time().minute() == other_offset.time().minute() && this_offset.time().second() == other_offset.time().second() } _ => false, } } } impl CookieExpiration { /// Indicates if the `Cookie` is expired as of *now*. pub fn is_expired(&self) -> bool { self.expires_by(&time::OffsetDateTime::now_utc()) } /// Indicates if the `Cookie` expires as of `utc_tm`. pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool { match *self { CookieExpiration::AtUtc(ref expire_tm) => *expire_tm <= *utc_tm, CookieExpiration::SessionEnd => false, } } } const MAX_RFC3339: time::OffsetDateTime = time::macros::date!(9999 - 12 - 31) .with_time(time::macros::time!(23:59:59)) .assume_utc(); impl From for CookieExpiration { fn from(max_age: u64) -> CookieExpiration { // make sure we don't trigger a panic! in Duration by restricting the seconds // to the max CookieExpiration::from(time::Duration::seconds(std::cmp::min( time::Duration::MAX.whole_seconds() as u64, max_age, ) as i64)) } } impl From for CookieExpiration { fn from(utc_tm: OffsetDateTime) -> CookieExpiration { CookieExpiration::AtUtc(utc_tm.min(MAX_RFC3339)) } } impl From for CookieExpiration { fn from(expiration: cookie::Expiration) -> CookieExpiration { match expiration { cookie::Expiration::DateTime(offset) => CookieExpiration::AtUtc(offset), cookie::Expiration::Session => CookieExpiration::SessionEnd, } } } impl From for CookieExpiration { fn from(duration: time::Duration) -> Self { // If delta-seconds is less than or equal to zero (0), let expiry-time // be the earliest representable date and time. Otherwise, let the // expiry-time be the current date and time plus delta-seconds seconds. let utc_tm = if duration.is_zero() { time::OffsetDateTime::UNIX_EPOCH } else { let now_utc = time::OffsetDateTime::now_utc(); let d = (MAX_RFC3339 - now_utc).min(duration); now_utc + d }; CookieExpiration::from(utc_tm) } } #[cfg(test)] mod tests { use super::CookieExpiration; use time; use crate::utils::test::*; #[test] fn max_age_bounds() { match CookieExpiration::from(time::Duration::MAX.whole_seconds() as u64 + 1) { CookieExpiration::AtUtc(_) => assert!(true), _ => assert!(false), } } #[test] fn expired() { let ma = CookieExpiration::from(0u64); // Max-Age<=0 indicates the cookie is expired assert!(ma.is_expired()); assert!(ma.expires_by(&in_days(-1))); } #[test] fn max_age() { let ma = CookieExpiration::from(60u64); assert!(!ma.is_expired()); assert!(ma.expires_by(&in_minutes(2))); } #[test] fn session_end() { // SessionEnd never "expires"; lives until end of session let se = CookieExpiration::SessionEnd; assert!(!se.is_expired()); assert!(!se.expires_by(&in_days(1))); assert!(!se.expires_by(&in_days(-1))); } #[test] fn at_utc() { { let expire_tmrw = CookieExpiration::from(in_days(1)); assert!(!expire_tmrw.is_expired()); assert!(expire_tmrw.expires_by(&in_days(2))); } { let expired_yest = CookieExpiration::from(in_days(-1)); assert!(expired_yest.is_expired()); assert!(!expired_yest.expires_by(&in_days(-2))); } } } cookie_store-0.21.1/src/cookie_path.rs000064400000000000000000000207221046102023000160170ustar 00000000000000#[cfg(feature = "serde")] use serde_derive::{Deserialize, Serialize}; use std::cmp::max; use std::ops::Deref; use url::Url; /// Returns true if `request_url` path-matches `path` per /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) pub fn is_match(path: &str, request_url: &Url) -> bool { CookiePath::parse(path).map_or(false, |cp| cp.matches(request_url)) } /// The path of a `Cookie` #[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CookiePath(String, bool); impl CookiePath { /// Determine if `request_url` path-matches this `CookiePath` per /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) pub fn matches(&self, request_url: &Url) -> bool { if request_url.cannot_be_a_base() { false } else { let request_path = request_url.path(); let cookie_path = &*self.0; // o The cookie-path and the request-path are identical. cookie_path == request_path || (request_path.starts_with(cookie_path) && (cookie_path.ends_with('/') || &request_path[cookie_path.len()..=cookie_path.len()] == "/")) } } /// Returns true if this `CookiePath` was set from a Path attribute; this allows us to /// distinguish from the case where Path was explicitly set to "/" pub fn is_from_path_attr(&self) -> bool { self.1 } // The user agent MUST use an algorithm equivalent to the following // algorithm to compute the default-path of a cookie: // // 1. Let uri-path be the path portion of the request-uri if such a // portion exists (and empty otherwise). For example, if the // request-uri contains just a path (and optional query string), // then the uri-path is that path (without the %x3F ("?") character // or query string), and if the request-uri contains a full // absoluteURI, the uri-path is the path component of that URI. // // 2. If the uri-path is empty or if the first character of the uri- // path is not a %x2F ("/") character, output %x2F ("/") and skip // the remaining steps. // // 3. If the uri-path contains no more than one %x2F ("/") character, // output %x2F ("/") and skip the remaining step. // // 4. Output the characters of the uri-path from the first character up // to, but not including, the right-most %x2F ("/"). /// Determine the default-path of `request_url` per /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) pub fn default_path(request_url: &Url) -> CookiePath { let cp = if request_url.cannot_be_a_base() { // non-relative path scheme, default to "/" (uri-path "empty", case 2) "/".into() } else { let path = request_url.path(); match path.rfind('/') { None => "/".into(), // no "/" in string, default to "/" (case 2) Some(i) => path[0..max(i, 1)].into(), // case 4 (subsumes case 3) } }; CookiePath(cp, false) } /// Attempt to parse `path` as a `CookiePath`; if unsuccessful, the default-path of /// `request_url` will be returned as the `CookiePath`. pub fn new(path: &str, request_url: &Url) -> CookiePath { match CookiePath::parse(path) { Some(cp) => cp, None => CookiePath::default_path(request_url), } } /// Attempt to parse `path` as a `CookiePath`. If `path` does not have a leading "/", /// `None` is returned. pub fn parse(path: &str) -> Option { if path.starts_with('/') { Some(CookiePath(String::from(path), true)) } else { None } } } impl AsRef for CookiePath { fn as_ref(&self) -> &str { &self.0 } } impl Deref for CookiePath { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl<'a> From<&'a CookiePath> for String { fn from(cp: &CookiePath) -> String { cp.0.clone() } } impl From for String { fn from(cp: CookiePath) -> String { cp.0 } } #[cfg(test)] mod tests { use super::CookiePath; use url::Url; #[test] fn default_path() { fn get_path(url: &str) -> String { CookiePath::default_path(&Url::parse(url).expect("unable to parse url in default_path")) .into() } assert_eq!(get_path("data:foobusbar"), "/"); assert_eq!(get_path("http://example.com"), "/"); assert_eq!(get_path("http://example.com/"), "/"); assert_eq!(get_path("http://example.com/foo"), "/"); assert_eq!(get_path("http://example.com/foo/"), "/foo"); assert_eq!(get_path("http://example.com//foo/"), "//foo"); assert_eq!(get_path("http://example.com/foo//"), "/foo/"); assert_eq!(get_path("http://example.com/foo/bus/bar"), "/foo/bus"); assert_eq!(get_path("http://example.com/foo//bus/bar"), "/foo//bus"); assert_eq!(get_path("http://example.com/foo/bus/bar/"), "/foo/bus/bar"); } fn do_match(exp: bool, cp: &str, rp: &str) { let url = Url::parse(&format!("http://example.com{}", rp)) .expect("unable to parse url in do_match"); let cp = CookiePath::parse(cp).expect("unable to parse CookiePath in do_match"); assert!( exp == cp.matches(&url), "\n>> {:?}\nshould{}match\n>> {:?}\n>> {:?}\n", cp, if exp { " " } else { " NOT " }, url, url.path() ); } fn is_match(cp: &str, rp: &str) { do_match(true, cp, rp); } fn is_mismatch(cp: &str, rp: &str) { do_match(false, cp, rp); } #[test] fn bad_paths() { assert!(CookiePath::parse("").is_none()); assert!(CookiePath::parse("a/foo").is_none()); } #[test] fn bad_path_defaults() { fn get_path(cp: &str, url: &str) -> String { CookiePath::new( cp, &Url::parse(url).expect("unable to parse url in bad_path_defaults"), ) .into() } assert_eq!(get_path("", "http://example.com/"), "/"); assert_eq!(get_path("a/foo", "http://example.com/"), "/"); assert_eq!(get_path("", "http://example.com/foo/bar"), "/foo"); assert_eq!(get_path("a/foo", "http://example.com/foo/bar"), "/foo"); assert_eq!(get_path("", "http://example.com/foo/bar/"), "/foo/bar"); assert_eq!(get_path("a/foo", "http://example.com/foo/bar/"), "/foo/bar"); } #[test] fn shortest_path() { is_match("/", "/"); } // A request-path path-matches a given cookie-path if at least one of // the following conditions holds: #[test] fn identical_paths() { // o The cookie-path and the request-path are identical. is_match("/foo/bus", "/foo/bus"); // identical is_mismatch("/foo/bus", "/foo/buss"); // trailing character is_mismatch("/foo/bus", "/zoo/bus"); // character mismatch is_mismatch("/foo/bus", "/zfoo/bus"); // leading character } #[test] fn cookie_path_prefix1() { // o The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/"). is_match("/foo/", "/foo/bus"); // cookie-path a prefix and ends in "/" is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but last char != "/" and first char in request-path ("o") after prefix != "/" } #[test] fn cookie_path_prefix2() { // o The cookie-path is a prefix of the request-path, and the first // character of the request-path that is not included in the cookie- // path is a %x2F ("/") character. is_match("/foo", "/foo/bus"); // cookie-path a prefix of request-path, and next char in request-path = "/" is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but next char in request-path ("o") != "/" } } cookie_store-0.21.1/src/cookie_store.rs000064400000000000000000002015501046102023000162170ustar 00000000000000use std::io::{BufRead, Write}; use std::ops::Deref; use cookie::Cookie as RawCookie; use log::debug; use url::Url; use crate::cookie::Cookie; use crate::cookie_domain::is_match as domain_match; use crate::cookie_path::is_match as path_match; use crate::utils::{is_http_scheme, is_secure}; use crate::CookieError; #[cfg(feature = "preserve_order")] use indexmap::IndexMap; #[cfg(not(feature = "preserve_order"))] use std::collections::HashMap; #[cfg(feature = "preserve_order")] type Map = IndexMap; #[cfg(not(feature = "preserve_order"))] type Map = HashMap; type NameMap = Map>; type PathMap = Map; type DomainMap = Map; #[derive(PartialEq, Clone, Debug, Eq)] pub enum StoreAction { /// The `Cookie` was successfully added to the store Inserted, /// The `Cookie` successfully expired a `Cookie` already in the store ExpiredExisting, /// The `Cookie` was added to the store, replacing an existing entry UpdatedExisting, } pub type StoreResult = Result; pub type InsertResult = Result; #[derive(Debug, Default, Clone)] /// An implementation for storing and retrieving [`Cookie`]s per the path and domain matching /// rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). pub struct CookieStore { /// Cookies stored by domain, path, then name cookies: DomainMap, #[cfg(feature = "public_suffix")] /// If set, enables [public suffix](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) rejection based on the provided `publicsuffix::List` public_suffix_list: Option, } impl CookieStore { #[deprecated( since = "0.14.1", note = "Please use the `get_request_values` function instead" )] /// Return an `Iterator` of the cookies for `url` in the store, suitable for submitting in an /// HTTP request. As the items are intended for use in creating a `Cookie` header in a GET request, /// they may contain only the `name` and `value` of a received cookie, eliding other parameters /// such as `path` or `expires`. For iteration over `Cookie` instances containing all data, please /// refer to [`CookieStore::matches`]. pub fn get_request_cookies(&self, url: &Url) -> impl Iterator> { self.matches(url).into_iter().map(|c| c.deref()) } /// Return an `Iterator` of the cookie (`name`, `value`) pairs for `url` in the store, suitable /// for use in the `Cookie` header of an HTTP request. For iteration over `Cookie` instances, /// please refer to [`CookieStore::matches`]. pub fn get_request_values(&self, url: &Url) -> impl Iterator { self.matches(url).into_iter().map(|c| c.name_value()) } /// Store the `cookies` received from `url` pub fn store_response_cookies>>( &mut self, cookies: I, url: &Url, ) { for cookie in cookies { if cookie.secure() != Some(true) || cfg!(feature = "log_secure_cookie_values") { debug!("inserting Set-Cookie '{:?}'", cookie); } else { debug!("inserting secure cookie '{}'", cookie.name()); } if let Err(e) = self.insert_raw(&cookie, url) { debug!("unable to store Set-Cookie: {:?}", e); } } } /// Specify a `publicsuffix::List` for the `CookieStore` to allow [public suffix /// matching](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) #[cfg(feature = "public_suffix")] pub fn with_suffix_list(self, psl: publicsuffix::List) -> CookieStore { CookieStore { cookies: self.cookies, public_suffix_list: Some(psl), } } /// Returns true if the `CookieStore` contains an __unexpired__ `Cookie` corresponding to the /// specified `domain`, `path`, and `name`. pub fn contains(&self, domain: &str, path: &str, name: &str) -> bool { self.get(domain, path, name).is_some() } /// Returns true if the `CookieStore` contains any (even an __expired__) `Cookie` corresponding /// to the specified `domain`, `path`, and `name`. pub fn contains_any(&self, domain: &str, path: &str, name: &str) -> bool { self.get_any(domain, path, name).is_some() } /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`, /// `path`, and `name`. pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'_>> { self.get_any(domain, path, name).and_then(|cookie| { if cookie.is_expired() { None } else { Some(cookie) } }) } /// Returns a mutable reference to the __unexpired__ `Cookie` corresponding to the specified /// `domain`, `path`, and `name`. fn get_mut(&mut self, domain: &str, path: &str, name: &str) -> Option<&mut Cookie<'static>> { self.get_mut_any(domain, path, name).and_then(|cookie| { if cookie.is_expired() { None } else { Some(cookie) } }) } /// Returns a reference to the (possibly __expired__) `Cookie` corresponding to the specified /// `domain`, `path`, and `name`. pub fn get_any(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'static>> { self.cookies.get(domain).and_then(|domain_cookies| { domain_cookies .get(path) .and_then(|path_cookies| path_cookies.get(name)) }) } /// Returns a mutable reference to the (possibly __expired__) `Cookie` corresponding to the /// specified `domain`, `path`, and `name`. fn get_mut_any( &mut self, domain: &str, path: &str, name: &str, ) -> Option<&mut Cookie<'static>> { self.cookies.get_mut(domain).and_then(|domain_cookies| { domain_cookies .get_mut(path) .and_then(|path_cookies| path_cookies.get_mut(name)) }) } /// Removes a `Cookie` from the store, returning the `Cookie` if it was in the store pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option> { #[cfg(not(feature = "preserve_order"))] fn map_remove(map: &mut Map, key: &Q) -> Option where K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, Q: std::cmp::Eq + std::hash::Hash + ?Sized, { map.remove(key) } #[cfg(feature = "preserve_order")] fn map_remove(map: &mut Map, key: &Q) -> Option where K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, Q: std::cmp::Eq + std::hash::Hash + ?Sized, { map.shift_remove(key) } let (removed, remove_domain) = match self.cookies.get_mut(domain) { None => (None, false), Some(domain_cookies) => { let (removed, remove_path) = match domain_cookies.get_mut(path) { None => (None, false), Some(path_cookies) => { let removed = map_remove(path_cookies, name); (removed, path_cookies.is_empty()) } }; if remove_path { map_remove(domain_cookies, path); (removed, domain_cookies.is_empty()) } else { (removed, false) } } }; if remove_domain { map_remove(&mut self.cookies, domain); } removed } /// Returns a collection of references to __unexpired__ cookies that path- and domain-match /// `request_url`, as well as having HttpOnly and Secure attributes compatible with the /// `request_url`. pub fn matches(&self, request_url: &Url) -> Vec<&Cookie<'static>> { // although we domain_match and path_match as we descend through the tree, we // still need to // do a full Cookie::matches() check in the last filter. Otherwise, we cannot // properly deal // with HostOnly Cookies. let cookies = self .cookies .iter() .filter(|&(d, _)| domain_match(d, request_url)) .flat_map(|(_, dcs)| { dcs.iter() .filter(|&(p, _)| path_match(p, request_url)) .flat_map(|(_, pcs)| { pcs.values() .filter(|c| !c.is_expired() && c.matches(request_url)) }) }); match (!is_http_scheme(request_url), !is_secure(request_url)) { (true, true) => cookies .filter(|c| !c.http_only().unwrap_or(false) && !c.secure().unwrap_or(false)) .collect(), (true, false) => cookies .filter(|c| !c.http_only().unwrap_or(false)) .collect(), (false, true) => cookies.filter(|c| !c.secure().unwrap_or(false)).collect(), (false, false) => cookies.collect(), } } /// Parses a new `Cookie` from `cookie_str` and inserts it into the store. pub fn parse(&mut self, cookie_str: &str, request_url: &Url) -> InsertResult { Cookie::parse(cookie_str, request_url) .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) } /// Converts a `cookie::Cookie` (from the `cookie` crate) into a `cookie_store::Cookie` and /// inserts it into the store. pub fn insert_raw(&mut self, cookie: &RawCookie<'_>, request_url: &Url) -> InsertResult { Cookie::try_from_raw_cookie(cookie, request_url) .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) } /// Inserts `cookie`, received from `request_url`, into the store, following the rules of the /// [IETF RFC6265 Storage Model](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3). If the /// `Cookie` is __unexpired__ and is successfully inserted, returns /// `Ok(StoreAction::Inserted)`. If the `Cookie` is __expired__ *and* matches an existing /// `Cookie` in the store, the existing `Cookie` wil be `expired()` and /// `Ok(StoreAction::ExpiredExisting)` will be returned. pub fn insert(&mut self, cookie: Cookie<'static>, request_url: &Url) -> InsertResult { if cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { // If the cookie was received from a "non-HTTP" API and the // cookie's http-only-flag is set, abort these steps and ignore the // cookie entirely. return Err(CookieError::NonHttpScheme); } #[cfg(feature = "public_suffix")] let mut cookie = cookie; #[cfg(feature = "public_suffix")] if let Some(ref psl) = self.public_suffix_list { // If the user agent is configured to reject "public suffixes" if cookie.domain.is_public_suffix(psl) { // and the domain-attribute is a public suffix: if cookie.domain.host_is_identical(request_url) { // If the domain-attribute is identical to the canonicalized // request-host: // Let the domain-attribute be the empty string. // (NB: at this point, an empty domain-attribute should be represented // as the HostOnly variant of CookieDomain) cookie.domain = crate::cookie_domain::CookieDomain::host_only(request_url)?; } else { // Otherwise: // Ignore the cookie entirely and abort these steps. return Err(CookieError::PublicSuffix); } } } if !cookie.domain.matches(request_url) { // If the canonicalized request-host does not domain-match the // domain-attribute: // Ignore the cookie entirely and abort these steps. return Err(CookieError::DomainMismatch); } // NB: we do not bail out above on is_expired(), as servers can remove a cookie // by sending // an expired one, so we need to do the old_cookie check below before checking // is_expired() on an incoming cookie { // At this point in parsing, any non-present Domain attribute should have been // converted into a HostOnly variant let cookie_domain = cookie .domain .as_cow() .ok_or_else(|| CookieError::UnspecifiedDomain)?; if let Some(old_cookie) = self.get_mut(&cookie_domain, &cookie.path, cookie.name()) { if old_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { // 2. If the newly created cookie was received from a "non-HTTP" // API and the old-cookie's http-only-flag is set, abort these // steps and ignore the newly created cookie entirely. return Err(CookieError::NonHttpScheme); } else if cookie.is_expired() { old_cookie.expire(); return Ok(StoreAction::ExpiredExisting); } } } if !cookie.is_expired() { Ok( if self .cookies .entry(String::from(&cookie.domain)) .or_insert_with(Map::new) .entry(String::from(&cookie.path)) .or_insert_with(Map::new) .insert(cookie.name().to_owned(), cookie) .is_none() { StoreAction::Inserted } else { StoreAction::UpdatedExisting }, ) } else { Err(CookieError::Expired) } } /// Clear the contents of the store pub fn clear(&mut self) { self.cookies.clear() } /// An iterator visiting all the __unexpired__ cookies in the store pub fn iter_unexpired<'a>(&'a self) -> impl Iterator> + 'a { self.cookies .values() .flat_map(|dcs| dcs.values()) .flat_map(|pcs| pcs.values()) .filter(|c| !c.is_expired()) } /// An iterator visiting all (including __expired__) cookies in the store pub fn iter_any<'a>(&'a self) -> impl Iterator> + 'a { self.cookies .values() .flat_map(|dcs| dcs.values()) .flat_map(|pcs| pcs.values()) } /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string` /// and write them to `writer` pub fn save(&self, writer: &mut W, cookie_to_string: F) -> StoreResult<()> where W: Write, F: Fn(&Cookie<'static>) -> Result, crate::Error: From, { for cookie in self.iter_unexpired().filter_map(|c| { if c.is_persistent() { Some(cookie_to_string(c)) } else { None } }) { writeln!(writer, "{}", cookie?)?; } Ok(()) } /// Serialize all (including __expired__ and __non-persistent__) cookies in the store with `cookie_to_string` and write them to `writer` pub fn save_incl_expired_and_nonpersistent( &self, writer: &mut W, cookie_to_string: F, ) -> StoreResult<()> where W: Write, F: Fn(&Cookie<'static>) -> Result, crate::Error: From, { for cookie in self.iter_any() { writeln!(writer, "{}", cookie_to_string(cookie)?)?; } Ok(()) } /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__ /// cookies pub fn load(reader: R, cookie_from_str: F) -> StoreResult where R: BufRead, F: Fn(&str) -> Result, E>, crate::Error: From, { CookieStore::load_from(reader, cookie_from_str, false) } /// Load cookies from `reader`, deserializing with `cookie_from_str`, loading both __unexpired__ /// and __expired__ cookies pub fn load_all(reader: R, cookie_from_str: F) -> StoreResult where R: BufRead, F: Fn(&str) -> Result, E>, crate::Error: From, { CookieStore::load_from(reader, cookie_from_str, true) } fn load_from( reader: R, cookie_from_str: F, include_expired: bool, ) -> StoreResult where R: BufRead, F: Fn(&str) -> Result, E>, crate::Error: From, { let cookies = reader.lines().map(|line_result| { line_result .map_err(Into::into) .and_then(|line| cookie_from_str(&line).map_err(crate::Error::from)) }); Self::from_cookies(cookies, include_expired) } /// Create a `CookieStore` from an iterator of `Cookie` values. When /// `include_expired` is `true`, both __expired__ and __unexpired__ cookies in the incoming /// iterator will be included in the produced `CookieStore`; otherwise, only /// __unexpired__ cookies will be included, and __expired__ cookies filtered /// out. pub fn from_cookies(iter: I, include_expired: bool) -> Result where I: IntoIterator, E>>, { let mut cookies = Map::new(); for cookie in iter { let cookie = cookie?; if include_expired || !cookie.is_expired() { cookies .entry(String::from(&cookie.domain)) .or_insert_with(Map::new) .entry(String::from(&cookie.path)) .or_insert_with(Map::new) .insert(cookie.name().to_owned(), cookie); } } Ok(Self { cookies, #[cfg(feature = "public_suffix")] public_suffix_list: None, }) } pub fn new( #[cfg(feature = "public_suffix")] public_suffix_list: Option, ) -> Self { Self { cookies: DomainMap::new(), #[cfg(feature = "public_suffix")] public_suffix_list, } } } #[cfg(feature = "serde_json")] /// Legacy serialization implementations. These methods do **not** produce/consume valid JSON output compatible with /// typical JSON libraries/tools. impl CookieStore { /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and /// write them to `writer` /// /// __NB__: this method does not produce valid JSON which can be directly loaded; such output /// must be loaded via the corresponding method [CookieStore::load_json]. For a more /// robust/universal /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this /// method. #[deprecated( since = "0.22.0", note = "See `cookie_store::serde` modules for more robust de/serialization options" )] pub fn save_json(&self, writer: &mut W) -> StoreResult<()> { self.save(writer, ::serde_json::to_string) } /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to JSON format and write them to `writer` /// /// __NB__: this method does not produce valid JSON which can be directly loaded; such output /// must be loaded via the corresponding method [CookieStore::load_json]. For a more /// robust/universal /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this /// method. #[deprecated( since = "0.22.0", note = "See `cookie_store::serde` modules for more robust de/serialization options" )] pub fn save_incl_expired_and_nonpersistent_json( &self, writer: &mut W, ) -> StoreResult<()> { self.save_incl_expired_and_nonpersistent(writer, ::serde_json::to_string) } /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies /// /// __NB__: this method does not expect true valid JSON; it is designed to load output /// from the corresponding method [CookieStore::save_json]. For a more robust/universal /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this /// method. #[deprecated( since = "0.22.0", note = "See `cookie_store::serde` modules for more robust de/serialization options" )] pub fn load_json(reader: R) -> StoreResult { CookieStore::load(reader, |cookie| ::serde_json::from_str(cookie)) } /// Load JSON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies /// /// __NB__: this method does not expect true valid JSON; it is designed to load output /// from the corresponding method [CookieStore::save_json]. For a more robust/universal /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this /// method. #[deprecated( since = "0.22.0", note = "See `cookie_store::serde` modules for more robust de/serialization options" )] pub fn load_json_all(reader: R) -> StoreResult { CookieStore::load_all(reader, |cookie| ::serde_json::from_str(cookie)) } } #[cfg(feature = "serde")] /// Legacy de/serialization implementation which elides the collection-nature of the contained /// cookies. Suitable for line-oriented cookie persistence, but prefer/consider /// `cookie_store::serde` modules for more universally consumable serialization formats. mod serde_legacy { use serde::de::{SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; impl Serialize for super::CookieStore { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.collect_seq(self.iter_unexpired().filter(|c| c.is_persistent())) } } struct CookieStoreVisitor; impl<'de> Visitor<'de> for CookieStoreVisitor { type Value = super::CookieStore; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "a sequence of cookies") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { super::CookieStore::from_cookies(std::iter::from_fn(|| seq.next_element().transpose()), false) } } impl<'de> Deserialize<'de> for super::CookieStore { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_seq(CookieStoreVisitor) } } } #[cfg(test)] mod tests { use super::CookieStore; use super::{InsertResult, StoreAction}; use crate::cookie::Cookie; use crate::CookieError; use ::cookie::Cookie as RawCookie; use time::OffsetDateTime; use crate::utils::test as test_utils; macro_rules! inserted { ($e: expr) => { assert_eq!(Ok(StoreAction::Inserted), $e) }; } macro_rules! updated { ($e: expr) => { assert_eq!(Ok(StoreAction::UpdatedExisting), $e) }; } macro_rules! expired_existing { ($e: expr) => { assert_eq!(Ok(StoreAction::ExpiredExisting), $e) }; } macro_rules! domain_mismatch { ($e: expr) => { assert_eq!(Err(CookieError::DomainMismatch), $e) }; } macro_rules! non_http_scheme { ($e: expr) => { assert_eq!(Err(CookieError::NonHttpScheme), $e) }; } macro_rules! non_rel_scheme { ($e: expr) => { assert_eq!(Err(CookieError::NonRelativeScheme), $e) }; } macro_rules! expired_err { ($e: expr) => { assert_eq!(Err(CookieError::Expired), $e) }; } macro_rules! values_are { ($store: expr, $url: expr, $values: expr) => {{ let mut matched_values = $store .matches(&test_utils::url($url)) .iter() .map(|c| &c.value()[..]) .collect::>(); matched_values.sort(); let mut values: Vec<&str> = $values; values.sort(); assert!( matched_values == values, "\n{:?}\n!=\n{:?}\n", matched_values, values ); }}; } fn add_cookie( store: &mut CookieStore, cookie: &str, url: &str, expires: Option, max_age: Option, ) -> InsertResult { store.insert( test_utils::make_cookie(cookie, url, expires, max_age), &test_utils::url(url), ) } fn make_match_store() -> CookieStore { let mut store = CookieStore::default(); inserted!(add_cookie( &mut store, "cookie1=1", "http://example.com/foo/bar", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie2=2; Secure", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie3=3; HttpOnly", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie4=4; Secure; HttpOnly", "https://example.com/sec/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie5=5", "http://example.com/foo/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie6=6", "http://example.com/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie7=7", "http://bar.example.com/foo/", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie8=8", "http://example.org/foo/bar", None, Some(60 * 5), )); inserted!(add_cookie( &mut store, "cookie9=9", "http://bar.example.org/foo/bar", None, Some(60 * 5), )); store } macro_rules! check_matches { ($store: expr) => {{ values_are!($store, "http://unknowndomain.org/foo/bar", vec![]); values_are!($store, "http://example.org/foo/bar", vec!["8"]); values_are!($store, "http://example.org/bus/bar", vec![]); values_are!($store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!($store, "http://bar.example.org/bus/bar", vec![]); values_are!( $store, "https://example.com/sec/foo", vec!["6", "4", "3", "2"] ); values_are!($store, "http://example.com/sec/foo", vec!["6", "3"]); values_are!($store, "ftp://example.com/sec/foo", vec!["6"]); values_are!($store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!( $store, "http://example.com/foo/bar/bus", vec!["1", "5", "6"] ); }}; } #[test] fn insert_raw() { let mut store = CookieStore::default(); inserted!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); non_rel_scheme!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("data:nonrelativescheme"), )); non_http_scheme!(store.insert_raw( &RawCookie::parse("cookie1=value1; HttpOnly").unwrap(), &test_utils::url("ftp://example.com/"), )); expired_existing!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=0").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); expired_err!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); updated!(store.insert_raw( &RawCookie::parse("cookie1=value1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); expired_existing!(store.insert_raw( &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); domain_mismatch!(store.insert_raw( &RawCookie::parse("cookie1=value1; Domain=bar.example.com").unwrap(), &test_utils::url("http://example.com/foo/bar"), )); } #[test] fn parse() { let mut store = CookieStore::default(); inserted!(store.parse( "cookie1=value1", &test_utils::url("http://example.com/foo/bar"), )); non_rel_scheme!(store.parse("cookie1=value1", &test_utils::url("data:nonrelativescheme"),)); non_http_scheme!(store.parse( "cookie1=value1; HttpOnly", &test_utils::url("ftp://example.com/"), )); expired_existing!(store.parse( "cookie1=value1; Max-Age=0", &test_utils::url("http://example.com/foo/bar"), )); expired_err!(store.parse( "cookie1=value1; Max-Age=-1", &test_utils::url("http://example.com/foo/bar"), )); updated!(store.parse( "cookie1=value1", &test_utils::url("http://example.com/foo/bar"), )); expired_existing!(store.parse( "cookie1=value1; Max-Age=-1", &test_utils::url("http://example.com/foo/bar"), )); domain_mismatch!(store.parse( "cookie1=value1; Domain=bar.example.com", &test_utils::url("http://example.com/foo/bar"), )); } #[test] fn domains() { let mut store = CookieStore::default(); // The user agent will reject cookies unless the Domain attribute // specifies a scope for the cookie that would include the origin // server. For example, the user agent will accept a cookie with a // Domain attribute of "example.com" or of "foo.example.com" from // foo.example.com, but the user agent will not accept a cookie with a // Domain attribute of "bar.example.com" or of "baz.foo.example.com". fn domain_cookie_from(domain: &str, request_url: &str) -> Cookie<'static> { let cookie_str = format!("cookie1=value1; Domain={}", domain); Cookie::parse(cookie_str, &test_utils::url(request_url)).unwrap() } { let request_url = test_utils::url("http://foo.example.com"); // foo.example.com can submit cookies for example.com and foo.example.com inserted!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); inserted!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); // not for bar.example.com domain_mismatch!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); // not for bar.foo.example.com domain_mismatch!(store.insert( domain_cookie_from("bar.foo.example.com", "http://bar.foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.foo.example.com", "http://bar.foo.example.com",), &request_url, )); } { let request_url = test_utils::url("http://bar.example.com"); // bar.example.com can submit for example.com and bar.example.com updated!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); inserted!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); // bar.example.com cannot submit for foo.example.com domain_mismatch!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); } { let request_url = test_utils::url("http://example.com"); // example.com can submit for example.com updated!(store.insert( domain_cookie_from("example.com", "http://foo.example.com",), &request_url, )); updated!(store.insert( domain_cookie_from(".example.com", "http://foo.example.com",), &request_url, )); // example.com cannot submit for foo.example.com or bar.example.com domain_mismatch!(store.insert( domain_cookie_from("foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".foo.example.com", "http://foo.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from("bar.example.com", "http://bar.example.com",), &request_url, )); domain_mismatch!(store.insert( domain_cookie_from(".bar.example.com", "http://bar.example.com",), &request_url, )); } } #[test] fn http_only() { let mut store = CookieStore::default(); let c = Cookie::parse( "cookie1=value1; HttpOnly", &test_utils::url("http://example.com/foo/bar"), ) .unwrap(); // cannot add a HttpOnly cookies from a non-http source non_http_scheme!(store.insert(c, &test_utils::url("ftp://example.com/foo/bar"),)); } #[test] fn clear() { let mut store = CookieStore::default(); inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); assert!(store.iter_any().any(|c| c.name_value() == ("cookie1", "value1")), "did not find expected cookie1=value1 cookie in store"); store.clear(); assert!(store.iter_any().count() == 0, "found unexpected cookies in cleared store"); } #[test] fn add_and_get() { let mut store = CookieStore::default(); assert!(store.get("example.com", "/foo", "cookie1").is_none()); inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo/bar", "cookie1").is_none()); assert!(store.get("example.com", "/foo", "cookie2").is_none()); assert!(store.get("example.org", "/foo", "cookie1").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); updated!(add_cookie( &mut store, "cookie1=value2", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); inserted!(add_cookie( &mut store, "cookie2=value3", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); inserted!(add_cookie( &mut store, "cookie3=value4; HttpOnly", "http://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); non_http_scheme!(add_cookie( &mut store, "cookie3=value5", "ftp://example.com/foo/bar", None, None, )); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); } #[test] fn matches() { let store = make_match_store(); check_matches!(&store); } fn matches_are(store: &CookieStore, url: &str, exp: Vec<&str>) { let matches = store .matches(&test_utils::url(url)) .iter() .map(|c| format!("{}={}", c.name(), c.value())) .collect::>(); for e in &exp { assert!( matches.iter().any(|m| &m[..] == *e), "{}: matches missing '{}'\nmatches: {:?}\n exp: {:?}", url, e, matches, exp ); } assert!( matches.len() == exp.len(), "{}: matches={:?} != exp={:?}", url, matches, exp ); } #[test] fn some_non_https_uris_are_secure() { // Matching the list in Firefox's regression test: // https://hg.mozilla.org/integration/autoland/rev/c4d13b3ca1e2 let secure_uris = vec![ "http://localhost", "http://localhost:1234", "http://127.0.0.1", "http://127.0.0.2", "http://127.1.0.1", "http://[::1]", ]; for secure_uri in secure_uris { let mut store = CookieStore::default(); inserted!(add_cookie( &mut store, "cookie1=1a; Secure", secure_uri, None, None, )); matches_are(&store, secure_uri, vec!["cookie1=1a"]); } } #[cfg(feature = "serde_json")] macro_rules! dump_json { ($e: expr, $i: ident) => {{ use serde_json; println!(""); println!( "==== {}: {} ====", $e, time::OffsetDateTime::now_utc() .format(crate::rfc3339_fmt::RFC3339_FORMAT) .unwrap() ); for c in $i.iter_any() { println!( "{} {}", if c.is_expired() { "XXXXX" } else if c.is_persistent() { "PPPPP" } else { " " }, serde_json::to_string(c).unwrap() ); println!("----------------"); } println!("================"); }}; } #[test] fn domain_collisions() { let mut store = CookieStore::default(); // - HostOnly, so no collisions here inserted!(add_cookie( &mut store, "cookie1=1a", "http://foo.bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie1=1b", "http://bus.example.com/", None, None, )); // - Suffix // both cookie2's domain-match bus.example.com inserted!(add_cookie( &mut store, "cookie2=2a; Domain=bus.example.com", "http://foo.bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie2=2b; Domain=example.com", "http://bus.example.com/", None, None, )); #[cfg(feature = "serde_json")] dump_json!("domain_collisions", store); matches_are( &store, "http://foo.bus.example.com/", vec!["cookie1=1a", "cookie2=2a", "cookie2=2b"], ); matches_are( &store, "http://bus.example.com/", vec!["cookie1=1b", "cookie2=2a", "cookie2=2b"], ); matches_are(&store, "http://example.com/", vec!["cookie2=2b"]); matches_are(&store, "http://foo.example.com/", vec!["cookie2=2b"]); } #[test] fn path_collisions() { let mut store = CookieStore::default(); // will be default-path: /foo/bar, and /foo, resp. // both should match /foo/bar/ inserted!(add_cookie( &mut store, "cookie3=3a", "http://bus.example.com/foo/bar/", None, None, )); inserted!(add_cookie( &mut store, "cookie3=3b", "http://bus.example.com/foo/", None, None, )); // - Path set explicitly inserted!(add_cookie( &mut store, "cookie4=4a; Path=/foo/bar/", "http://bus.example.com/", None, None, )); inserted!(add_cookie( &mut store, "cookie4=4b; Path=/foo/", "http://bus.example.com/", None, None, )); #[cfg(feature = "serde_json")] dump_json!("path_collisions", store); matches_are( &store, "http://bus.example.com/foo/bar/", vec!["cookie3=3a", "cookie3=3b", "cookie4=4a", "cookie4=4b"], ); // Agrees w/ chrome, but not FF... FF also sends cookie4=4a, but this should be // a path-match // fail since request-uri /foo/bar is a *prefix* of the cookie path /foo/bar/ matches_are( &store, "http://bus.example.com/foo/bar", vec!["cookie3=3a", "cookie3=3b", "cookie4=4b"], ); matches_are( &store, "http://bus.example.com/foo/ba", vec!["cookie3=3b", "cookie4=4b"], ); matches_are( &store, "http://bus.example.com/foo/", vec!["cookie3=3b", "cookie4=4b"], ); // Agrees w/ chrome, but not FF... FF also sends cookie4=4b, but this should be // a path-match // fail since request-uri /foo is a *prefix* of the cookie path /foo/ matches_are(&store, "http://bus.example.com/foo", vec!["cookie3=3b"]); matches_are(&store, "http://bus.example.com/fo", vec![]); matches_are(&store, "http://bus.example.com/", vec![]); matches_are(&store, "http://bus.example.com", vec![]); } #[cfg(feature = "serde_json")] #[allow(deprecated)] mod serde_json_tests { use super::{CookieStore, StoreAction, add_cookie, make_match_store}; use crate::cookie::Cookie; use crate::CookieError; use crate::utils::test as test_utils; macro_rules! has_str { ($e: expr, $i: ident) => {{ let val = std::str::from_utf8(&$i[..]).unwrap(); assert!(val.contains($e), "exp: {}\nval: {}", $e, val); }}; } macro_rules! not_has_str { ($e: expr, $i: ident) => {{ let val = std::str::from_utf8(&$i[..]).unwrap(); assert!(!val.contains($e), "exp: {}\nval: {}", $e, val); }}; } #[test] fn save_json() { let mut output = vec![]; let mut store = CookieStore::default(); store.save_json(&mut output).unwrap(); assert_eq!("", std::str::from_utf8(&output[..]).unwrap()); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); store.save_json(&mut output).unwrap(); assert_eq!("", std::str::from_utf8(&output[..]).unwrap()); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); output.clear(); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); output.clear(); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "https://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); output.clear(); } #[test] fn serialize_json() { let mut output = vec![]; let mut store = CookieStore::default(); serde_json::to_writer(&mut output, &store).unwrap(); assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap()); output.clear(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); serde_json::to_writer(&mut output, &store).unwrap(); assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap()); output.clear(); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); output.clear(); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); output.clear(); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "https://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); output.clear(); } #[test] fn load_json() { let mut store = CookieStore::default(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); let mut output = vec![]; store.save_json(&mut output).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); let store = CookieStore::load_json(&output[..]).unwrap(); assert!(store.get("example.com", "/foo", "cookie0").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); assert!( store .get("foo.example.com", "/foo/", "cookie4") .unwrap() .value() == "value4" ); assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); output.clear(); let store = make_match_store(); store.save_json(&mut output).unwrap(); let store = CookieStore::load_json(&output[..]).unwrap(); check_matches!(&store); } #[test] fn deserialize_json() { let mut store = CookieStore::default(); // non-persistent cookie, should not be saved inserted!(add_cookie( &mut store, "cookie0=value0", "http://example.com/foo/bar", None, None, )); // persistent cookie, Max-Age inserted!(add_cookie( &mut store, "cookie1=value1", "http://example.com/foo/bar", None, Some(10), )); // persistent cookie, Expires inserted!(add_cookie( &mut store, "cookie2=value2", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie3=value3; Domain=example.com", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie4=value4; Path=/foo/", "http://foo.example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie5=value5", "http://127.0.0.1/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie6=value6", "http://[::1]/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie7=value7; Secure", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); inserted!(add_cookie( &mut store, "cookie8=value8; HttpOnly", "http://example.com/foo/bar", Some(test_utils::in_days(1)), None, )); let mut output = vec![]; serde_json::to_writer(&mut output, &store).unwrap(); not_has_str!("cookie0=value0", output); has_str!("cookie1=value1", output); has_str!("cookie2=value2", output); has_str!("cookie3=value3", output); has_str!("cookie4=value4", output); has_str!("cookie5=value5", output); has_str!("cookie6=value6", output); has_str!("cookie7=value7; Secure", output); has_str!("cookie8=value8; HttpOnly", output); let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); assert!(store.get("example.com", "/foo", "cookie0").is_none()); assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); assert!( store .get("foo.example.com", "/foo/", "cookie4") .unwrap() .value() == "value4" ); assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); output.clear(); let store = make_match_store(); serde_json::to_writer(&mut output, &store).unwrap(); let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); check_matches!(&store); } #[test] fn expiry_json() { let mut store = make_match_store(); let request_url = test_utils::url("http://foo.example.com"); let expired_cookie = Cookie::parse("cookie1=value1; Max-Age=-1", &request_url).unwrap(); expired_err!(store.insert(expired_cookie, &request_url)); check_matches!(&store); match store.get_mut("example.com", "/", "cookie6") { Some(cookie) => cookie.expire(), None => unreachable!(), } values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1", "5"]); match store.get_any("example.com", "/", "cookie6") { Some(cookie) => assert!(cookie.is_expired()), None => unreachable!(), } // inserting an expired cookie that matches an existing cookie should expire // the existing let request_url = test_utils::url("http://example.com/foo/"); let expired_cookie = Cookie::parse("cookie5=value5; Max-Age=-1", &request_url).unwrap(); expired_existing!(store.insert(expired_cookie, &request_url)); values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); match store.get_any("example.com", "/foo", "cookie5") { Some(cookie) => assert!(cookie.is_expired()), None => unreachable!(), } // save and loading the store should drop any expired cookies let mut output = vec![]; store.save_json(&mut output).unwrap(); store = CookieStore::load_json(&output[..]).unwrap(); values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); values_are!(store, "http://example.org/foo/bar", vec!["8"]); values_are!(store, "http://example.org/bus/bar", vec![]); values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); values_are!(store, "http://bar.example.org/bus/bar", vec![]); values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); values_are!(store, "http://example.com/sec/foo", vec!["3"]); values_are!(store, "ftp://example.com/sec/foo", vec![]); values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); assert!(store.get_any("example.com", "/", "cookie6").is_none()); assert!(store.get_any("example.com", "/foo", "cookie5").is_none()); } #[test] fn non_persistent_json() { let mut store = make_match_store(); check_matches!(&store); let request_url = test_utils::url("http://example.com/tmp/"); let non_persistent = Cookie::parse("cookie10=value10", &request_url).unwrap(); inserted!(store.insert(non_persistent, &request_url)); match store.get("example.com", "/tmp", "cookie10") { None => unreachable!(), Some(cookie) => assert_eq!("value10", cookie.value()), } // save and loading the store should drop any non-persistent cookies let mut output = vec![]; store.save_json(&mut output).unwrap(); store = CookieStore::load_json(&output[..]).unwrap(); check_matches!(&store); assert!(store.get("example.com", "/tmp", "cookie10").is_none()); assert!(store.get_any("example.com", "/tmp", "cookie10").is_none()); } } } cookie_store-0.21.1/src/lib.rs000064400000000000000000000056251046102023000143050ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] //! # cookie_store //! Provides an implementation for storing and retrieving [`Cookie`]s per the path and domain matching //! rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). //! //! ## Example //! Please refer to the [reqwest_cookie_store](https://crates.io/crates/reqwest_cookie_store) for //! an example of using this library along with [reqwest](https://crates.io/crates/reqwest). //! //! ## Feature flags #![doc = document_features::document_features!()] use idna; pub use ::cookie::{Cookie as RawCookie, ParseError as RawCookieParseError}; mod cookie; pub use crate::cookie::Error as CookieError; pub use crate::cookie::{Cookie, CookieResult}; mod cookie_domain; pub use crate::cookie_domain::CookieDomain; mod cookie_expiration; pub use crate::cookie_expiration::CookieExpiration; mod cookie_path; pub use crate::cookie_path::CookiePath; mod cookie_store; pub use crate::cookie_store::{CookieStore, StoreAction}; #[cfg(feature = "serde")] pub mod serde; mod utils; #[derive(Debug)] pub struct IdnaErrors(idna::Errors); impl std::fmt::Display for IdnaErrors { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "IDNA errors: {:#?}", self.0) } } impl std::error::Error for IdnaErrors {} impl From for IdnaErrors { fn from(e: idna::Errors) -> Self { IdnaErrors(e) } } pub type Error = Box; pub type Result = std::result::Result; #[cfg(feature = "serde")] pub(crate) mod rfc3339_fmt { pub(crate) const RFC3339_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"); pub(super) fn serialize(t: &time::OffsetDateTime, serializer: S) -> Result where S: serde::Serializer, { use serde::ser::Error; // An explicit format string is used here, instead of time::format_description::well_known::Rfc3339, to explicitly // utilize the 'Z' terminator instead of +00:00 format for Zulu time. let s = t.format(&RFC3339_FORMAT).map_err(|e| { println!("{}", e); S::Error::custom(format!( "Could not parse datetime '{}' as RFC3339 UTC format: {}", t, e )) })?; serializer.serialize_str(&s) } pub(super) fn deserialize<'de, D>(t: D) -> Result where D: serde::Deserializer<'de>, { use serde::{de::Error, Deserialize}; let s = String::deserialize(t)?; time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err( |e| { D::Error::custom(format!( "Could not parse string '{}' as RFC3339 UTC format: {}", s, e )) }, ) } } cookie_store-0.21.1/src/serde/json.rs000064400000000000000000000130651046102023000156070ustar 00000000000000//! De/serialization via the JSON format //! Requires feature `serde_json` use std::io::{BufRead, Write}; use crate::cookie_store::{StoreResult, CookieStore}; /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies. /// __NB__: This function is not compatible with data produced by [CookieStore::save_json] or /// [CookieStore::save_incl_expired_and_nonpersistent_json]. pub fn load(reader: R) -> StoreResult { super::load(reader, |cookies| serde_json::from_str(cookies)) } /// Load JSON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies. /// __NB__: This function is not compatible with data produced by [CookieStore::save_json] or /// [CookieStore::save_incl_expired_and_nonpersistent_json]. pub fn load_all(reader: R) -> StoreResult { super::load_all(reader, |cookies| serde_json::from_str(cookies)) } /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and /// write them to `writer`. /// __NB__: This function does not produce data compatible with [CookieStore::load_json] or /// [CookieStore::load_json_all]. pub fn save(cookie_store: &CookieStore, writer: &mut W) -> StoreResult<()> { super::save(cookie_store, writer, ::serde_json::to_string_pretty) } /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to JSON format and write them to `writer`. /// __NB__: This function does not produce data compatible with [CookieStore::load_json] or /// [CookieStore::load_json_all]. pub fn save_incl_expired_and_nonpersistent( cookie_store: &CookieStore, writer: &mut W, ) -> StoreResult<()> { super::save_incl_expired_and_nonpersistent(cookie_store, writer, ::serde_json::to_string_pretty) } #[cfg(test)] mod tests { use std::io::BufWriter; use super::{ save_incl_expired_and_nonpersistent, save }; use super::{ load, load_all }; fn cookie() -> String { r#"[ { "raw_cookie": "2=two; SameSite=None; Secure; Path=/; Expires=Tue, 03 Aug 2100 00:38:37 GMT", "path": [ "/", true ], "domain": { "HostOnly": "test.com" }, "expires": { "AtUtc": "2100-08-03T00:38:37Z" } } ] "# .to_string() } fn cookie_expired() -> String { r#"[ { "raw_cookie": "1=one; SameSite=None; Secure; Path=/; Expires=Thu, 03 Aug 2000 00:38:37 GMT", "path": [ "/", true ], "domain": { "HostOnly": "test.com" }, "expires": { "AtUtc": "2000-08-03T00:38:37Z" } } ] "# .to_string() } #[test] fn check_count() { let cookie = cookie(); let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 1); let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 1); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); } #[test] fn check_count_expired() { let cookie = cookie_expired(); let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 0); assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 0); let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 0); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); } } cookie_store-0.21.1/src/serde/ron.rs000064400000000000000000000120131046102023000154240ustar 00000000000000//! De/serialization via the RON format //! Requires feature `serde_ron` use std::io::{BufRead, Write}; use crate::cookie_store::{StoreResult, CookieStore}; /// Load RON-formatted cookies from `reader`, skipping any __expired__ cookies pub fn load(reader: R) -> StoreResult { super::load(reader, |cookies| ron::from_str(cookies)) } /// Load RON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies pub fn load_all(reader: R) -> StoreResult { super::load_all(reader, |cookies| ron::from_str(cookies)) } /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and /// write them to `writer` pub fn save(cookie_store: &CookieStore, writer: &mut W) -> StoreResult<()> { super::save(cookie_store, writer, |string| { ::ron::ser::to_string_pretty(string, ron::ser::PrettyConfig::default()) }) } /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to RON format and write them to `writer` pub fn save_incl_expired_and_nonpersistent( cookie_store: &CookieStore, writer: &mut W, ) -> StoreResult<()> { super::save_incl_expired_and_nonpersistent(cookie_store, writer, |string| { ::ron::ser::to_string_pretty(string, ron::ser::PrettyConfig::default()) }) } #[cfg(test)] mod tests { use std::io::BufWriter; use super::{load, load_all}; use super::{ save_incl_expired_and_nonpersistent, save }; fn cookie() -> String { r#"[ ( raw_cookie: "2=two; SameSite=None; Secure; Path=/; Expires=Tue, 03 Aug 2100 00:38:37 GMT", path: ("/", true), domain: HostOnly("test.com"), expires: AtUtc("2100-08-03T00:38:37Z"), ), ] "#.to_string() } fn cookie_expired() -> String { r#"[ ( raw_cookie: "1=one; SameSite=None; Secure; Path=/; Expires=Thu, 03 Aug 2000 00:38:37 GMT", path: ("/", true), domain: HostOnly("test.com"), expires: AtUtc("2000-08-03T00:38:37Z"), ), ] "#.to_string() } #[test] fn check_count() { let cookie = cookie(); let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 1); let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 1); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); } #[test] fn check_count_expired() { let cookie = cookie_expired(); let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 0); assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 0); let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 0); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!("[]\n", string); let mut writer = BufWriter::new(Vec::new()); save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); assert_eq!(cookie, string); } } cookie_store-0.21.1/src/serde.rs000064400000000000000000000052201046102023000146300ustar 00000000000000//! De/serialization functionality //! Requires feature `serde` use std::io::{BufRead, Write}; use crate::{Cookie, cookie_store::StoreResult, CookieStore}; #[cfg(feature = "serde_json")] pub mod json; #[cfg(feature = "serde_ron")] pub mod ron; /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__ /// cookies pub fn load(reader: R, cookies_from_str: F) -> StoreResult where R: BufRead, F: Fn(&str) -> Result>, E>, crate::Error: From, { load_from(reader, cookies_from_str, false) } /// Load cookies from `reader`, deserializing with `cookie_from_str`, loading both __unexpired__ /// and __expired__ cookies pub fn load_all(reader: R, cookies_from_str: F) -> StoreResult where R: BufRead, F: Fn(&str) -> Result>, E>, crate::Error: From, { load_from(reader, cookies_from_str, true) } fn load_from( mut reader: R, cookies_from_str: F, include_expired: bool, ) -> StoreResult where R: BufRead, F: Fn(&str) -> Result>, E>, crate::Error: From, { let mut cookie_store = String::new(); reader.read_to_string(&mut cookie_store)?; let cookies = cookies_from_str(&cookie_store)?; CookieStore::from_cookies( cookies.into_iter().map(|cookies| Ok(cookies)), include_expired, ) } /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string` /// and write them to `writer` pub fn save( cookie_store: &CookieStore, writer: &mut W, cookies_to_string: F, ) -> StoreResult<()> where W: Write, F: Fn(&Vec>) -> Result, crate::Error: From, { let mut cookies = Vec::new(); for cookie in cookie_store.iter_unexpired() { if cookie.is_persistent() { cookies.push(cookie.clone()); } } let cookies = cookies_to_string(&cookies); writeln!(writer, "{}", cookies?)?; Ok(()) } /// Serialize all (including __expired__ and __non-persistent__) cookies in the store with `cookie_to_string` and write them to `writer` pub fn save_incl_expired_and_nonpersistent( cookie_store: &CookieStore, writer: &mut W, cookies_to_string: F, ) -> StoreResult<()> where W: Write, F: Fn(&Vec>) -> Result, crate::Error: From, { let mut cookies = Vec::new(); for cookie in cookie_store.iter_any() { cookies.push(cookie.clone()); } let cookies = cookies_to_string(&cookies); writeln!(writer, "{}", cookies?)?; Ok(()) } cookie_store-0.21.1/src/utils.rs000064400000000000000000000042451046102023000146740ustar 00000000000000use std::net::{Ipv4Addr, Ipv6Addr}; use url::Url; use url::{Host, ParseError as UrlError}; pub trait IntoUrl { fn into_url(self) -> Result; } impl IntoUrl for Url { fn into_url(self) -> Result { Ok(self) } } impl<'a> IntoUrl for &'a str { fn into_url(self) -> Result { Url::parse(self) } } impl<'a> IntoUrl for &'a String { fn into_url(self) -> Result { Url::parse(self) } } pub fn is_http_scheme(url: &Url) -> bool { url.scheme().starts_with("http") } pub fn is_host_name(host: &str) -> bool { host.parse::().is_err() && host.parse::().is_err() } pub fn is_secure(url: &Url) -> bool { if url.scheme() == "https" { return true; } if let Some(u) = url.host() { match u { Host::Domain(d) => d == "localhost", Host::Ipv4(ip) => ip.is_loopback(), Host::Ipv6(ip) => ip.is_loopback(), } } else { false } } #[cfg(test)] pub mod test { use crate::cookie::Cookie; use time::{Duration, OffsetDateTime}; use url::Url; #[inline] pub fn url(url: &str) -> Url { Url::parse(url).unwrap() } #[inline] pub fn make_cookie<'a>( cookie: &str, url_str: &str, expires: Option, max_age: Option, ) -> Cookie<'a> { Cookie::parse( format!( "{}{}{}", cookie, expires.map_or(String::from(""), |e| format!( "; Expires={}", e.format(time::macros::format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT")).unwrap() )), max_age.map_or(String::from(""), |m| format!("; Max-Age={}", m)) ), &url(url_str), ) .unwrap() } #[inline] pub fn in_days(days: i64) -> OffsetDateTime { OffsetDateTime::now_utc() + Duration::days(days) } #[inline] pub fn in_minutes(mins: i64) -> OffsetDateTime { OffsetDateTime::now_utc() + Duration::minutes(mins) } }