├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cliff.toml ├── release.sh └── src ├── cookie.rs ├── cookie_domain.rs ├── cookie_expiration.rs ├── cookie_path.rs ├── cookie_store.rs ├── lib.rs ├── serde.rs ├── serde ├── json.rs └── ron.rs └── utils.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ci-pass: 14 | name: CI is green 15 | runs-on: ubuntu-latest 16 | needs: 17 | - msrv 18 | - build 19 | - test 20 | steps: 21 | - run: exit 0 22 | 23 | msrv: 24 | name: MSRV 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Get MSRV package metadata 32 | id: metadata 33 | run: cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' >> $GITHUB_OUTPUT 34 | 35 | - name: Install rust (${{ steps.metadata.outputs.msrv }}) 36 | uses: dtolnay/rust-toolchain@master 37 | with: 38 | toolchain: ${{ steps.metadata.outputs.msrv }} 39 | 40 | - name: Pin time and idna_adapter versions 41 | run: | 42 | cargo update 43 | cargo update -p time --precise 0.3.20 44 | cargo update -p idna_adapter --precise 1.1.0 45 | 46 | - uses: Swatinem/rust-cache@v2 47 | 48 | - name: Check 49 | run: cargo check 50 | 51 | build: 52 | name: ${{ matrix.name }} 53 | 54 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 55 | 56 | strategy: 57 | matrix: 58 | name: 59 | - linux / stable 60 | include: 61 | - name: linux / stable 62 | 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v3 66 | 67 | - name: Install rust 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | toolchain: ${{ matrix.rust || 'stable' }} 71 | profile: minimal 72 | override: true 73 | 74 | - name: Build 75 | uses: actions-rs/cargo@v1 76 | with: 77 | command: build 78 | args: --verbose --all-features 79 | 80 | test: 81 | name: ${{ matrix.name }} 82 | 83 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 84 | 85 | strategy: 86 | matrix: 87 | name: 88 | - linux / stable 89 | include: 90 | - name: linux / stable 91 | 92 | steps: 93 | - name: Checkout 94 | uses: actions/checkout@v3 95 | 96 | - name: Install rust 97 | uses: actions-rs/toolchain@v1 98 | with: 99 | toolchain: ${{ matrix.rust || 'stable' }} 100 | profile: minimal 101 | override: true 102 | 103 | - name: Run tests 104 | uses: actions-rs/cargo@v1 105 | with: 106 | command: test 107 | args: --verbose --all-features 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.21.1] - 2024-11-09 4 | 5 | ### Documentation 6 | 7 | - Update CONTRIBUTORS.md 8 | - Switch to using `document-feature` for genearating feature flag documentation 9 | - Improve documentation around features 10 | - Add documentation around legacy serialization vs. `serde` module 11 | 12 | ### Features 13 | 14 | - Gate serialization behind features `serde{,_json,_ron}` 15 | 16 | ### Miscellaneous Tasks 17 | 18 | - Bump `indexmap` to `2.6.0` 19 | 20 | ### Build 21 | 22 | - Set `rust-version=1.63.0` 23 | - Add `serde_json` as a default feature 24 | - Specify feature dependencies with explcit `dep:` 25 | 26 | ### Ci 27 | 28 | - Split ci check step `build` into `build` + `test`. Add `msrv` job 29 | 30 | ### Refact 31 | 32 | - De/serialize through simple `Vec` instead of `CookieStoreSerialized` 33 | - Collect legacy `mod cookie_store` serialization impl 34 | - Rename `mod serialization` -> `serde`; split out `json`, `ron` 35 | - Split `ron` and `json` serialization tests 36 | - Reorganize tests to respect `serde*` features 37 | - Move serialization into dedicated `mod serialization` 38 | 39 | ## [0.21.0] - 2024-02-08 40 | 41 | ### Miscellaneous Tasks 42 | 43 | - Update CONTRIBUTORS.md 44 | 45 | ### Ci 46 | 47 | - Add missing v0.20.1 CHANGELOG entries 48 | - Rm `--topo-order` from `git-cliff` call 49 | 50 | ## [0.20.1] - 2024-02-08 51 | 52 | ### Bug Fixes 53 | 54 | - Pub use `cookie_store::StoreAction` 55 | - Need to maintain 0.20.x series for [patch] behavior to work 56 | 57 | ### Miscellaneous Tasks 58 | 59 | - Update CONTRIBUTORS.md 60 | 61 | ## [0.20.0] - 2023-06-17 62 | 63 | ### Features 64 | 65 | - Re-export dependency cookie 66 | - Add `CookieStore::new()` 67 | 68 | ### Styling 69 | 70 | - Rust_fmt changes 71 | 72 | ## [0.19.1] - 2023-06-17 73 | 74 | ### Ci 75 | 76 | - Allow specification of last tag to generate CHANGELOG from 77 | - Fix git-cliff args for latest release 78 | - Allow serde and serde_derive to compile in parallel 79 | - Check tag format in release.sh 80 | 81 | ## [0.19.0] - 2022-11-05 82 | 83 | ### Bug Fixes 84 | 85 | - Store clone of original raw cookie 86 | 87 | ### Cookie_store 88 | 89 | - Fix missing raw cookie elements 90 | 91 | ## [0.18.0] - 2022-10-25 92 | 93 | ### Documentation 94 | 95 | - Remove old `reqwest_impl` REMOVAL notice 96 | 97 | ### Features 98 | 99 | - Make logging secure cookie values opt-in 100 | 101 | ### Miscellaneous Tasks 102 | 103 | - Dependency bumps 104 | - Update CONTRIBUTORS 105 | - Update to idna 0.3 106 | - Do not use annotated tags in release.sh 107 | - Prepare version item for `release.sh` 108 | - Prepare to start using `git-cliff` 109 | 110 | ### Styling 111 | 112 | - Cargo fmt 113 | - Fix release.sh comments/whitespace 114 | 115 | ### Build 116 | 117 | - Expose feature `wasm-bindgen` 118 | 119 | ### Cookie_store 120 | 121 | - Derive clone for CookieStore 122 | - Add API to save all cookies 123 | 124 | ### Rename 125 | 126 | - New `save_all` methods to emphasize divergence from RFC behavior 127 | 128 | ## [0.17.0] - 2022-08-30 129 | 130 | ### Miscellaneous Tasks 131 | 132 | - Prepare version item for `release.sh` 133 | - Prepare to start using `git-cliff` 134 | 135 | ## [0.16.1] 136 | * Export `cookie_domain::CookieDomain` as `pub` 137 | * Export `pub use cookie_expiration::CookieExpiration` 138 | * Export `pub use cookie_path::CookiePath` 139 | * Make `CookieStore::from_cookies` pub 140 | * Add methods `CookieStore::load_json_all` and `CookieStore::load_all` to allow 141 | for loading both __unexpired__ and __expired__ cookies. 142 | 143 | ## [0.16.0] 144 | * Update of dependencies in public API in `0.15.2` should have qualified as minor version bump 145 | 146 | ## [0.15.2] __YANKED__ 147 | * Upgrade dependencies 148 | 149 | ## [0.15.1] 150 | * 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). 151 | 152 | ## [0.15.0] 153 | * deprecation in `v0.14.1` should have qualified as minor version bump 154 | * Upgrade dependencies 155 | 156 | ## [0.14.1] 157 | * Improve documentation on `CookieStore::get_request_cookies` 158 | * Introduce alternative `CookieStore::get_request_values`, mark `CookieStore::get_request_cookies` as deprecated, and suggest usage of `get_request_values` instead. 159 | 160 | ## [0.14.0] 161 | * **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. 162 | * **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. 163 | * `reqwest` is no longer an optional dependency, it is now a `dev-dependency` for doctests. 164 | * Only enable the needed features for `reqwest` (@blyxxyz) 165 | * Upgrade `publisuffix` dependency to `v2` (@rushmorem) 166 | * Remove unused dev-dependencies 167 | 168 | ## [0.13.3] 169 | * Fix attributes & configuration for feature support in docs.rs 170 | 171 | ## [0.13.0] 172 | * Introduce optional feature `reqwest_impl`, providing implementations of the `reqwest::cookie::CookieStore` trait 173 | * Upgrade to `reqwest 0.11.2` 174 | * Upgrade to `env_logger 0.8` 175 | * Upgrade to `pretty_assertions 0.7` 176 | * Upgrade to `cookie 0.15` 177 | 178 | ## [0.12.0] 179 | * Upgrade to `cookie 0.14` 180 | * Upgrade to `time 0.2` 181 | 182 | ## [0.11.0] 183 | * Implement `{De,}Serialize` for `CookieStore` (@Felerius) 184 | 185 | ## [0.10.0] 186 | * introduce optional feature `preserve_order` which maintains cookies in insertion order. 187 | 188 | ## [0.9.0] 189 | * remove `try_from` dependency again now that `reqwest` minimum rust version is bumped 190 | * upgrade to `url 2.0` (@benesch) 191 | * Upgrade to `idna 0.2` 192 | 193 | ## [0.8.0] 194 | * Remove dependency on `failure` (seanmonstar) 195 | 196 | ## [0.7.0] 197 | * Revert removal of `try_from` dependency 198 | 199 | ## [0.6.0] 200 | * Upgrades to `cookies` v0.12 201 | * Drop dependency `try_from` in lieu of `std::convert::TryFrom` (@oherrala) 202 | * Drop dependency on `serde_derive`, rely on `serde` only (@oherrala) 203 | 204 | ## [0.4.0] 205 | * Update to Rust 2018 edition 206 | 207 | ## [0.3.1] 208 | 209 | * Upgrades to `cookies` v0.11 210 | * Minor dependency upgrades 211 | 212 | ## [0.3] 213 | 214 | * Upgrades to `reqwest` v0.9 215 | * Replaces `error-chain` with `failure` 216 | 217 | ## [0.2] 218 | 219 | * Removes separate `ReqwestSession::ErrorKind`. Added as variant `::ErrorKind::Reqwest` instead. 220 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * @dcampbell24 2 | * @joshstoik1 3 | * @LMJian 4 | * @nickelc 5 | * @dtolnay 6 | * ian-fox 7 | * @1One1 8 | * @lmd0 9 | * @SabrinaJewson 10 | * @Expyron 11 | * @LukeMathWalker 12 | * @blyxxyz 13 | * @rushmorem 14 | * @koushiro 15 | * @Felerius 16 | * @oherrala 17 | * @seanmonstar 18 | * @twistedfall 19 | * @erickt 20 | * @benesch 21 | * @kpcyrd 22 | * @pfernie 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Patrick Fernie "] 3 | description = "Implementation of Cookie storage and retrieval" 4 | name = "cookie_store" 5 | version = "0.21.1" # managed by release.sh 6 | edition = "2021" 7 | rust-version = "1.63.0" 8 | repository = "https://github.com/pfernie/cookie_store" 9 | documentation = "https://docs.rs/cookie_store" 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | keywords = ["cookie", "jar", "store", "http"] # free text 13 | categories = ["web-programming::http-client", "web-programming"] # https://crates.io/category_slugs 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [features] 20 | default = ["public_suffix", "serde_json"] 21 | 22 | ## uses `indexmap::IndexMap` in lieu of HashMap internally, so cookies are maintained in insertion/creation order 23 | preserve_order = ["dep:indexmap"] 24 | ## Add support for public suffix lists, as provided by [publicsuffix](https://crates.io/crates/publicsuffix). 25 | public_suffix = ["dep:publicsuffix"] 26 | ## Enables transitive feature `time/wasm-bindgen`; necessary in `wasm` contexts. 27 | wasm-bindgen = ["time/wasm-bindgen"] 28 | 29 | ## Enable logging the values of cookies marked 'secure', off by default as values may be sensitive 30 | log_secure_cookie_values = [] 31 | 32 | #! ### Serialization 33 | ## Supports generic (format-agnostic) de/serialization for a `CookieStore`. Adds dependencies `serde` and `serde_derive`. 34 | serde = ["dep:serde", "dep:serde_derive"] 35 | ## Supports de/serialization for a `CookieStore` via the JSON format. Enables feature `serde` and adds depenency `serde_json`. 36 | serde_json = ["serde", "dep:serde_json"] 37 | ## Supports de/serialization for a `CookieStore` via the RON format. Enables feature `serde` and adds depenency `ron`. 38 | serde_ron = ["serde", "dep:ron"] 39 | 40 | [dependencies] 41 | document-features = "0.2.10" 42 | idna = "1.0" 43 | log = "0.4.17" 44 | time = "0.3.16" 45 | url = "2.3.1" 46 | 47 | indexmap = { version = "2.6.0", optional = true } 48 | 49 | publicsuffix = { version = "2.2.3", optional = true } 50 | 51 | # serialization dependencies 52 | serde = { version = "1.0.147", optional = true } 53 | serde_derive = { version = "1.0.147", optional = true } 54 | serde_json = { version = "1.0.87", optional = true } 55 | ron = { version = "0.8.1", optional = true } 56 | 57 | [dependencies.cookie] 58 | features = ["percent-encode"] 59 | version = "0.18.0" 60 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/pfernie/cookie_store/actions/workflows/ci.yml/badge.svg)](https://github.com/pfernie/cookie_store/actions/workflows/ci.yml) 2 | [![Documentation](https://docs.rs/cookie_store/badge.svg)](https://docs.rs/cookie_store) 3 | 4 | Provides an implementation for storing and retrieving `Cookie`s per the path and domain matching 5 | rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). 6 | 7 | ## Features 8 | 9 | * `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. 10 | 11 | ## Usage with [reqwest](https://crates.io/crates/reqwest) 12 | 13 | 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`. 14 | 15 | ## License 16 | This project is licensed and distributed under the terms of both the MIT license and Apache License (Version 2.0). 17 | 18 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) 19 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog\n 7 | """ 8 | # template for the changelog body 9 | # https://tera.netlify.app/docs/#introduction 10 | body = """ 11 | {% if version %}\ 12 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 13 | {% else %}\ 14 | ## [unreleased] 15 | {% endif %}\ 16 | {% for group, commits in commits | group_by(attribute="group") %} 17 | ### {{ group | upper_first }} 18 | {% for commit in commits %} 19 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 20 | {% endfor %} 21 | {% endfor %}\n 22 | """ 23 | # remove the leading and trailing whitespace from the template 24 | trim = true 25 | # changelog footer 26 | footer = """ 27 | """ 28 | 29 | [git] 30 | # parse the commits based on https://www.conventionalcommits.org 31 | conventional_commits = true 32 | # filter out the commits that are not conventional 33 | filter_unconventional = true 34 | # process each line of a commit as an individual commit 35 | split_commits = false 36 | # regex for preprocessing the commit messages 37 | commit_preprocessors = [ 38 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, 39 | ] 40 | # regex for parsing and grouping commits 41 | commit_parsers = [ 42 | { message = "^feat", group = "Features"}, 43 | { message = "^fix", group = "Bug Fixes"}, 44 | { message = "^doc", group = "Documentation"}, 45 | { message = "^perf", group = "Performance"}, 46 | { message = "^refactor", group = "Refactor"}, 47 | { message = "^style", group = "Styling"}, 48 | { message = "^test", group = "Testing"}, 49 | { message = "^chore\\(release\\): prepare for", skip = true}, 50 | { message = "^chore", group = "Miscellaneous Tasks"}, 51 | { body = ".*security", group = "Security"}, 52 | ] 53 | # filter out the commits that are not matched by commit parsers 54 | filter_commits = false 55 | # glob pattern for matching git tags 56 | tag_pattern = "v[0-9]*" 57 | # regex for skipping tags 58 | skip_tags = "v0.1.0-beta.1" 59 | # regex for ignoring tags 60 | ignore_tags = "" 61 | # sort the tags chronologically 62 | date_order = false 63 | # sort the commits inside sections by oldest/newest order 64 | sort_commits = "oldest" 65 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git_status=$(git status --porcelain) 4 | if [[ ! -z $git_status ]]; then 5 | echo -e "\e[31muncommitted state:\e[0m" 6 | git status -s 7 | echo -e "\e[31mplease commit or tidy uncommitted state before running release\e[0m" 8 | exit 9 | fi 10 | 11 | # takes the tag as an argument (e.g. v0.1.0) 12 | if [ -n "$1" ]; then 13 | if ! $(echo "${1}" | grep -q '^v[0-9]\+\.[0-9]\+\.[0-9]\+$'); then 14 | echo -e "\e[31m${1} not a version of the expected format; please use v#.#.# format\e[0m" 15 | exit 16 | fi 17 | since_flag="--unreleased" 18 | if [[ ! -z "${2}" ]]; then 19 | since_flag="${2}" 20 | fi 21 | # update the version 22 | msg="# managed by release.sh" 23 | sed "s/^version = .* $msg$/version = \"${1#v}\" $msg/" -i Cargo.toml 24 | # update the changelog 25 | git cliff --sort newest $since_flag --tag "$1" --prepend CHANGELOG.md 26 | git diff 27 | echo -e -n "\e[33mProceed? \e[0m" 28 | read -n 1 -s -p "[y/N] " proceed 29 | echo 30 | if [[ "${proceed}" != "y" ]]; then 31 | echo -e "\e[31maborting; leaving dirty state:\e[0m" 32 | git status -s 33 | exit 34 | fi 35 | git add -A 36 | git commit -m "chore(release): prepare for $1" 37 | git show 38 | # generate a changelog for the tag message 39 | export GIT_CLIFF_TEMPLATE="\ 40 | {% for group, commits in commits | group_by(attribute=\"group\") %} 41 | {{ group | upper_first }}\ 42 | {% for commit in commits %} 43 | - {% if commit.breaking %}(breaking) {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end=\"\") }})\ 44 | {% endfor %} 45 | {% endfor %}" 46 | changelog=$(git cliff --sort newest $since_flag --strip all) 47 | git tag "$1" -m "Release $1" -m "$changelog" 48 | git show -q "$1" 49 | else 50 | echo "warn: please provide a tag" 51 | fi 52 | -------------------------------------------------------------------------------- /src/cookie.rs: -------------------------------------------------------------------------------- 1 | use crate::cookie_domain::CookieDomain; 2 | use crate::cookie_expiration::CookieExpiration; 3 | use crate::cookie_path::CookiePath; 4 | 5 | use crate::utils::{is_http_scheme, is_secure}; 6 | use cookie::{Cookie as RawCookie, CookieBuilder as RawCookieBuilder, ParseError}; 7 | #[cfg(feature = "serde")] 8 | use serde_derive::{Deserialize, Serialize}; 9 | use std::borrow::Cow; 10 | use std::convert::TryFrom; 11 | use std::fmt; 12 | use std::ops::Deref; 13 | use time; 14 | use url::Url; 15 | 16 | #[derive(Debug, Clone, PartialEq, Eq)] 17 | pub enum Error { 18 | /// Cookie had attribute HttpOnly but was received from a request-uri which was not an http 19 | /// scheme 20 | NonHttpScheme, 21 | /// Cookie did not specify domain but was received from non-relative-scheme request-uri from 22 | /// which host could not be determined 23 | NonRelativeScheme, 24 | /// Cookie received from a request-uri that does not domain-match 25 | DomainMismatch, 26 | /// Cookie is Expired 27 | Expired, 28 | /// `cookie::Cookie` Parse error 29 | Parse, 30 | #[cfg(feature = "public_suffix")] 31 | /// Cookie specified a public suffix domain-attribute that does not match the canonicalized 32 | /// request-uri host 33 | PublicSuffix, 34 | /// Tried to use a CookieDomain variant of `Empty` or `NotPresent` in a context requiring a Domain value 35 | UnspecifiedDomain, 36 | } 37 | 38 | impl std::error::Error for Error {} 39 | 40 | impl fmt::Display for Error { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!( 43 | f, 44 | "{}", 45 | match *self { 46 | Error::NonHttpScheme => 47 | "request-uri is not an http scheme but HttpOnly attribute set", 48 | Error::NonRelativeScheme => { 49 | "request-uri is not a relative scheme; cannot determine host" 50 | } 51 | Error::DomainMismatch => "request-uri does not domain-match the cookie", 52 | Error::Expired => "attempted to utilize an Expired Cookie", 53 | Error::Parse => "unable to parse string as cookie::Cookie", 54 | #[cfg(feature = "public_suffix")] 55 | Error::PublicSuffix => "domain-attribute value is a public suffix", 56 | Error::UnspecifiedDomain => "domain-attribute is not specified", 57 | } 58 | ) 59 | } 60 | } 61 | 62 | // cookie::Cookie::parse returns Result 63 | impl From for Error { 64 | fn from(_: ParseError) -> Error { 65 | Error::Parse 66 | } 67 | } 68 | 69 | pub type CookieResult<'a> = Result, Error>; 70 | 71 | /// A cookie conforming more closely to [IETF RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) 72 | #[derive(PartialEq, Clone, Debug)] 73 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 74 | pub struct Cookie<'a> { 75 | /// The parsed Set-Cookie data 76 | #[cfg_attr(feature = "serde", serde(serialize_with = "serde_raw_cookie::serialize"))] 77 | #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_raw_cookie::deserialize"))] 78 | raw_cookie: RawCookie<'a>, 79 | /// The Path attribute from a Set-Cookie header or the default-path as 80 | /// determined from 81 | /// the request-uri 82 | pub path: CookiePath, 83 | /// The Domain attribute from a Set-Cookie header, or a HostOnly variant if no 84 | /// non-empty Domain attribute 85 | /// found 86 | pub domain: CookieDomain, 87 | /// For a persistent Cookie (see [IETF RFC6265 Section 88 | /// 5.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3)), 89 | /// the expiration time as defined by the Max-Age or Expires attribute, 90 | /// otherwise SessionEnd, 91 | /// indicating a non-persistent `Cookie` that should expire at the end of the 92 | /// session 93 | pub expires: CookieExpiration, 94 | } 95 | 96 | #[cfg(feature = "serde")] 97 | mod serde_raw_cookie { 98 | use cookie::Cookie as RawCookie; 99 | use serde::de::Error; 100 | use serde::de::Unexpected; 101 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 102 | use std::str::FromStr; 103 | 104 | pub fn serialize(cookie: &RawCookie<'_>, serializer: S) -> Result 105 | where 106 | S: Serializer, 107 | { 108 | cookie.to_string().serialize(serializer) 109 | } 110 | 111 | pub fn deserialize<'a, D>(deserializer: D) -> Result, D::Error> 112 | where 113 | D: Deserializer<'a>, 114 | { 115 | let cookie = String::deserialize(deserializer)?; 116 | match RawCookie::from_str(&cookie) { 117 | Ok(cookie) => Ok(cookie), 118 | Err(_) => Err(D::Error::invalid_value( 119 | Unexpected::Str(&cookie), 120 | &"a cookie string", 121 | )), 122 | } 123 | } 124 | } 125 | 126 | impl<'a> Cookie<'a> { 127 | /// Whether this `Cookie` should be included for `request_url` 128 | pub fn matches(&self, request_url: &Url) -> bool { 129 | self.path.matches(request_url) 130 | && self.domain.matches(request_url) 131 | && (!self.raw_cookie.secure().unwrap_or(false) || is_secure(request_url)) 132 | && (!self.raw_cookie.http_only().unwrap_or(false) || is_http_scheme(request_url)) 133 | } 134 | 135 | /// Should this `Cookie` be persisted across sessions? 136 | pub fn is_persistent(&self) -> bool { 137 | match self.expires { 138 | CookieExpiration::AtUtc(_) => true, 139 | CookieExpiration::SessionEnd => false, 140 | } 141 | } 142 | 143 | /// Expire this cookie 144 | pub fn expire(&mut self) { 145 | self.expires = CookieExpiration::from(0u64); 146 | } 147 | 148 | /// Return whether the `Cookie` is expired *now* 149 | pub fn is_expired(&self) -> bool { 150 | self.expires.is_expired() 151 | } 152 | 153 | /// Indicates if the `Cookie` expires as of `utc_tm`. 154 | pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool { 155 | self.expires.expires_by(utc_tm) 156 | } 157 | 158 | /// Parses a new `cookie_store::Cookie` from `cookie_str`. 159 | pub fn parse(cookie_str: S, request_url: &Url) -> CookieResult<'a> 160 | where 161 | S: Into>, 162 | { 163 | Cookie::try_from_raw_cookie(&RawCookie::parse(cookie_str)?, request_url) 164 | } 165 | 166 | /// Create a new `cookie_store::Cookie` from a `cookie::Cookie` (from the `cookie` crate) 167 | /// received from `request_url`. 168 | pub fn try_from_raw_cookie(raw_cookie: &RawCookie<'a>, request_url: &Url) -> CookieResult<'a> { 169 | if raw_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { 170 | // If the cookie was received from a "non-HTTP" API and the 171 | // cookie's http-only-flag is set, abort these steps and ignore the 172 | // cookie entirely. 173 | return Err(Error::NonHttpScheme); 174 | } 175 | 176 | let domain = match CookieDomain::try_from(raw_cookie) { 177 | // 6. If the domain-attribute is non-empty: 178 | Ok(d @ CookieDomain::Suffix(_)) => { 179 | if !d.matches(request_url) { 180 | // If the canonicalized request-host does not domain-match the 181 | // domain-attribute: 182 | // Ignore the cookie entirely and abort these steps. 183 | Err(Error::DomainMismatch) 184 | } else { 185 | // Otherwise: 186 | // Set the cookie's host-only-flag to false. 187 | // Set the cookie's domain to the domain-attribute. 188 | Ok(d) 189 | } 190 | } 191 | Err(_) => Err(Error::Parse), 192 | // Otherwise: 193 | // Set the cookie's host-only-flag to true. 194 | // Set the cookie's domain to the canonicalized request-host. 195 | _ => CookieDomain::host_only(request_url), 196 | }?; 197 | 198 | let path = raw_cookie 199 | .path() 200 | .as_ref() 201 | .and_then(|p| CookiePath::parse(p)) 202 | .unwrap_or_else(|| CookiePath::default_path(request_url)); 203 | 204 | // per RFC6265, Max-Age takes precedence, then Expires, otherwise is Session 205 | // only 206 | let expires = if let Some(max_age) = raw_cookie.max_age() { 207 | CookieExpiration::from(max_age) 208 | } else if let Some(expiration) = raw_cookie.expires() { 209 | CookieExpiration::from(expiration) 210 | } else { 211 | CookieExpiration::SessionEnd 212 | }; 213 | 214 | Ok(Cookie { 215 | raw_cookie: raw_cookie.clone(), 216 | path, 217 | expires, 218 | domain, 219 | }) 220 | } 221 | 222 | pub fn into_owned(self) -> Cookie<'static> { 223 | Cookie { 224 | raw_cookie: self.raw_cookie.into_owned(), 225 | path: self.path, 226 | domain: self.domain, 227 | expires: self.expires, 228 | } 229 | } 230 | } 231 | 232 | impl<'a> Deref for Cookie<'a> { 233 | type Target = RawCookie<'a>; 234 | fn deref(&self) -> &Self::Target { 235 | &self.raw_cookie 236 | } 237 | } 238 | 239 | impl<'a> From> for RawCookie<'a> { 240 | fn from(cookie: Cookie<'a>) -> RawCookie<'static> { 241 | let mut builder = 242 | RawCookieBuilder::new(cookie.name().to_owned(), cookie.value().to_owned()); 243 | 244 | // Max-Age is relative, will not have same meaning now, so only set `Expires`. 245 | match cookie.expires { 246 | CookieExpiration::AtUtc(utc_tm) => { 247 | builder = builder.expires(utc_tm); 248 | } 249 | CookieExpiration::SessionEnd => {} 250 | } 251 | 252 | if cookie.path.is_from_path_attr() { 253 | builder = builder.path(String::from(cookie.path)); 254 | } 255 | 256 | if let CookieDomain::Suffix(s) = cookie.domain { 257 | builder = builder.domain(s); 258 | } 259 | 260 | builder.build() 261 | } 262 | } 263 | 264 | #[cfg(test)] 265 | mod tests { 266 | use super::Cookie; 267 | use crate::cookie_domain::CookieDomain; 268 | use crate::cookie_expiration::CookieExpiration; 269 | use cookie::Cookie as RawCookie; 270 | use time::{Duration, OffsetDateTime}; 271 | use url::Url; 272 | 273 | use crate::utils::test as test_utils; 274 | 275 | fn cmp_domain(cookie: &str, url: &str, exp: CookieDomain) { 276 | let ua = test_utils::make_cookie(cookie, url, None, None); 277 | assert!(ua.domain == exp, "\n{:?}", ua); 278 | } 279 | 280 | #[test] 281 | fn no_domain() { 282 | let url = test_utils::url("http://example.com/foo/bar"); 283 | cmp_domain( 284 | "cookie1=value1", 285 | "http://example.com/foo/bar", 286 | CookieDomain::host_only(&url).expect("unable to parse domain"), 287 | ); 288 | } 289 | 290 | // per RFC6265: 291 | // If the attribute-value is empty, the behavior is undefined. However, 292 | // the user agent SHOULD ignore the cookie-av entirely. 293 | #[test] 294 | fn empty_domain() { 295 | let url = test_utils::url("http://example.com/foo/bar"); 296 | cmp_domain( 297 | "cookie1=value1; Domain=", 298 | "http://example.com/foo/bar", 299 | CookieDomain::host_only(&url).expect("unable to parse domain"), 300 | ); 301 | } 302 | 303 | #[test] 304 | fn mismatched_domain() { 305 | let ua = Cookie::parse( 306 | "cookie1=value1; Domain=notmydomain.com", 307 | &test_utils::url("http://example.com/foo/bar"), 308 | ); 309 | assert!(ua.is_err(), "{:?}", ua); 310 | } 311 | 312 | #[test] 313 | fn domains() { 314 | fn domain_from(domain: &str, request_url: &str, is_some: bool) { 315 | let cookie_str = format!("cookie1=value1; Domain={}", domain); 316 | let raw_cookie = RawCookie::parse(cookie_str).unwrap(); 317 | let cookie = Cookie::try_from_raw_cookie(&raw_cookie, &test_utils::url(request_url)); 318 | assert_eq!(is_some, cookie.is_ok()) 319 | } 320 | // The user agent will reject cookies unless the Domain attribute 321 | // specifies a scope for the cookie that would include the origin 322 | // server. For example, the user agent will accept a cookie with a 323 | // Domain attribute of "example.com" or of "foo.example.com" from 324 | // foo.example.com, but the user agent will not accept a cookie with a 325 | // Domain attribute of "bar.example.com" or of "baz.foo.example.com". 326 | domain_from("example.com", "http://foo.example.com", true); 327 | domain_from(".example.com", "http://foo.example.com", true); 328 | domain_from("foo.example.com", "http://foo.example.com", true); 329 | domain_from(".foo.example.com", "http://foo.example.com", true); 330 | 331 | domain_from("oo.example.com", "http://foo.example.com", false); 332 | domain_from("myexample.com", "http://foo.example.com", false); 333 | domain_from("bar.example.com", "http://foo.example.com", false); 334 | domain_from("baz.foo.example.com", "http://foo.example.com", false); 335 | } 336 | 337 | #[test] 338 | fn httponly() { 339 | let c = RawCookie::parse("cookie1=value1; HttpOnly").unwrap(); 340 | let url = Url::parse("ftp://example.com/foo/bar").unwrap(); 341 | let ua = Cookie::try_from_raw_cookie(&c, &url); 342 | assert!(ua.is_err(), "{:?}", ua); 343 | } 344 | 345 | #[test] 346 | fn identical_domain() { 347 | cmp_domain( 348 | "cookie1=value1; Domain=example.com", 349 | "http://example.com/foo/bar", 350 | CookieDomain::Suffix(String::from("example.com")), 351 | ); 352 | } 353 | 354 | #[test] 355 | fn identical_domain_leading_dot() { 356 | cmp_domain( 357 | "cookie1=value1; Domain=.example.com", 358 | "http://example.com/foo/bar", 359 | CookieDomain::Suffix(String::from("example.com")), 360 | ); 361 | } 362 | 363 | #[test] 364 | fn identical_domain_two_leading_dots() { 365 | cmp_domain( 366 | "cookie1=value1; Domain=..example.com", 367 | "http://..example.com/foo/bar", 368 | CookieDomain::Suffix(String::from(".example.com")), 369 | ); 370 | } 371 | 372 | #[test] 373 | fn upper_case_domain() { 374 | cmp_domain( 375 | "cookie1=value1; Domain=EXAMPLE.com", 376 | "http://example.com/foo/bar", 377 | CookieDomain::Suffix(String::from("example.com")), 378 | ); 379 | } 380 | 381 | fn cmp_path(cookie: &str, url: &str, exp: &str) { 382 | let ua = test_utils::make_cookie(cookie, url, None, None); 383 | assert!(String::from(ua.path.clone()) == exp, "\n{:?}", ua); 384 | } 385 | 386 | #[test] 387 | fn no_path() { 388 | // no Path specified 389 | cmp_path("cookie1=value1", "http://example.com/foo/bar/", "/foo/bar"); 390 | cmp_path("cookie1=value1", "http://example.com/foo/bar", "/foo"); 391 | cmp_path("cookie1=value1", "http://example.com/foo", "/"); 392 | cmp_path("cookie1=value1", "http://example.com/", "/"); 393 | cmp_path("cookie1=value1", "http://example.com", "/"); 394 | } 395 | 396 | #[test] 397 | fn empty_path() { 398 | // Path specified with empty value 399 | cmp_path( 400 | "cookie1=value1; Path=", 401 | "http://example.com/foo/bar/", 402 | "/foo/bar", 403 | ); 404 | cmp_path( 405 | "cookie1=value1; Path=", 406 | "http://example.com/foo/bar", 407 | "/foo", 408 | ); 409 | cmp_path("cookie1=value1; Path=", "http://example.com/foo", "/"); 410 | cmp_path("cookie1=value1; Path=", "http://example.com/", "/"); 411 | cmp_path("cookie1=value1; Path=", "http://example.com", "/"); 412 | } 413 | 414 | #[test] 415 | fn invalid_path() { 416 | // Invalid Path specified (first character not /) 417 | cmp_path( 418 | "cookie1=value1; Path=baz", 419 | "http://example.com/foo/bar/", 420 | "/foo/bar", 421 | ); 422 | cmp_path( 423 | "cookie1=value1; Path=baz", 424 | "http://example.com/foo/bar", 425 | "/foo", 426 | ); 427 | cmp_path("cookie1=value1; Path=baz", "http://example.com/foo", "/"); 428 | cmp_path("cookie1=value1; Path=baz", "http://example.com/", "/"); 429 | cmp_path("cookie1=value1; Path=baz", "http://example.com", "/"); 430 | } 431 | 432 | #[test] 433 | fn path() { 434 | // Path specified, single / 435 | cmp_path( 436 | "cookie1=value1; Path=/baz", 437 | "http://example.com/foo/bar/", 438 | "/baz", 439 | ); 440 | // Path specified, multiple / (for valid attribute-value on path, take full 441 | // string) 442 | cmp_path( 443 | "cookie1=value1; Path=/baz/", 444 | "http://example.com/foo/bar/", 445 | "/baz/", 446 | ); 447 | } 448 | 449 | // expiry-related tests 450 | #[inline] 451 | fn in_days(days: i64) -> OffsetDateTime { 452 | OffsetDateTime::now_utc() + Duration::days(days) 453 | } 454 | #[inline] 455 | fn in_minutes(mins: i64) -> OffsetDateTime { 456 | OffsetDateTime::now_utc() + Duration::minutes(mins) 457 | } 458 | 459 | #[test] 460 | fn max_age_bounds() { 461 | let ua = test_utils::make_cookie( 462 | "cookie1=value1", 463 | "http://example.com/foo/bar", 464 | None, 465 | Some(9223372036854776), 466 | ); 467 | assert!(match ua.expires { 468 | CookieExpiration::AtUtc(_) => true, 469 | _ => false, 470 | }); 471 | } 472 | 473 | #[test] 474 | fn max_age() { 475 | let ua = test_utils::make_cookie( 476 | "cookie1=value1", 477 | "http://example.com/foo/bar", 478 | None, 479 | Some(60), 480 | ); 481 | assert!(!ua.is_expired()); 482 | assert!(ua.expires_by(&in_minutes(2))); 483 | } 484 | 485 | #[test] 486 | fn expired() { 487 | let ua = test_utils::make_cookie( 488 | "cookie1=value1", 489 | "http://example.com/foo/bar", 490 | None, 491 | Some(0u64), 492 | ); 493 | assert!(ua.is_expired()); 494 | assert!(ua.expires_by(&in_days(-1))); 495 | let ua = test_utils::make_cookie( 496 | "cookie1=value1; Max-Age=0", 497 | "http://example.com/foo/bar", 498 | None, 499 | None, 500 | ); 501 | assert!(ua.is_expired()); 502 | assert!(ua.expires_by(&in_days(-1))); 503 | let ua = test_utils::make_cookie( 504 | "cookie1=value1; Max-Age=-1", 505 | "http://example.com/foo/bar", 506 | None, 507 | None, 508 | ); 509 | assert!(ua.is_expired()); 510 | assert!(ua.expires_by(&in_days(-1))); 511 | } 512 | 513 | #[test] 514 | fn session_end() { 515 | let ua = 516 | test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None); 517 | assert!(match ua.expires { 518 | CookieExpiration::SessionEnd => true, 519 | _ => false, 520 | }); 521 | assert!(!ua.is_expired()); 522 | assert!(!ua.expires_by(&in_days(1))); 523 | assert!(!ua.expires_by(&in_days(-1))); 524 | } 525 | 526 | #[test] 527 | fn expires_tmrw_at_utc() { 528 | let ua = test_utils::make_cookie( 529 | "cookie1=value1", 530 | "http://example.com/foo/bar", 531 | Some(in_days(1)), 532 | None, 533 | ); 534 | assert!(!ua.is_expired()); 535 | assert!(ua.expires_by(&in_days(2))); 536 | } 537 | 538 | #[test] 539 | fn expired_yest_at_utc() { 540 | let ua = test_utils::make_cookie( 541 | "cookie1=value1", 542 | "http://example.com/foo/bar", 543 | Some(in_days(-1)), 544 | None, 545 | ); 546 | assert!(ua.is_expired()); 547 | assert!(!ua.expires_by(&in_days(-2))); 548 | } 549 | 550 | #[test] 551 | fn is_persistent() { 552 | let ua = 553 | test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None); 554 | assert!(!ua.is_persistent()); // SessionEnd 555 | let ua = test_utils::make_cookie( 556 | "cookie1=value1", 557 | "http://example.com/foo/bar", 558 | Some(in_days(1)), 559 | None, 560 | ); 561 | assert!(ua.is_persistent()); // AtUtc from Expires 562 | let ua = test_utils::make_cookie( 563 | "cookie1=value1", 564 | "http://example.com/foo/bar", 565 | Some(in_days(1)), 566 | Some(60), 567 | ); 568 | assert!(ua.is_persistent()); // AtUtc from Max-Age 569 | } 570 | 571 | #[test] 572 | fn max_age_overrides_expires() { 573 | // Expires indicates expiration yesterday, but Max-Age indicates expiry in 1 574 | // minute 575 | let ua = test_utils::make_cookie( 576 | "cookie1=value1", 577 | "http://example.com/foo/bar", 578 | Some(in_days(-1)), 579 | Some(60), 580 | ); 581 | assert!(!ua.is_expired()); 582 | assert!(ua.expires_by(&in_minutes(2))); 583 | } 584 | 585 | // A request-path path-matches a given cookie-path if at least one of 586 | // the following conditions holds: 587 | // o The cookie-path and the request-path are identical. 588 | // o The cookie-path is a prefix of the request-path, and the last 589 | // character of the cookie-path is %x2F ("/"). 590 | // o The cookie-path is a prefix of the request-path, and the first 591 | // character of the request-path that is not included in the cookie- 592 | // path is a %x2F ("/") character. 593 | #[test] 594 | fn matches() { 595 | fn do_match(exp: bool, cookie: &str, src_url: &str, request_url: Option<&str>) { 596 | let ua = test_utils::make_cookie(cookie, src_url, None, None); 597 | let request_url = request_url.unwrap_or(src_url); 598 | assert!( 599 | exp == ua.matches(&Url::parse(request_url).unwrap()), 600 | "\n>> {:?}\nshould{}match\n>> {:?}\n", 601 | ua, 602 | if exp { " " } else { " NOT " }, 603 | request_url 604 | ); 605 | } 606 | fn is_match(cookie: &str, url: &str, request_url: Option<&str>) { 607 | do_match(true, cookie, url, request_url); 608 | } 609 | fn is_mismatch(cookie: &str, url: &str, request_url: Option<&str>) { 610 | do_match(false, cookie, url, request_url); 611 | } 612 | 613 | // match: request-path & cookie-path (defaulted from request-uri) identical 614 | is_match("cookie1=value1", "http://example.com/foo/bar", None); 615 | // mismatch: request-path & cookie-path do not match 616 | is_mismatch( 617 | "cookie1=value1", 618 | "http://example.com/bus/baz/", 619 | Some("http://example.com/foo/bar"), 620 | ); 621 | is_mismatch( 622 | "cookie1=value1; Path=/bus/baz", 623 | "http://example.com/foo/bar", 624 | None, 625 | ); 626 | // match: cookie-path a prefix of request-path and last character of 627 | // cookie-path is / 628 | is_match( 629 | "cookie1=value1", 630 | "http://example.com/foo/bar", 631 | Some("http://example.com/foo/bar"), 632 | ); 633 | is_match( 634 | "cookie1=value1; Path=/foo/", 635 | "http://example.com/foo/bar", 636 | None, 637 | ); 638 | // mismatch: cookie-path a prefix of request-path but last character of 639 | // cookie-path is not / 640 | // and first character of request-path not included in cookie-path is not / 641 | is_mismatch( 642 | "cookie1=value1", 643 | "http://example.com/fo/", 644 | Some("http://example.com/foo/bar"), 645 | ); 646 | is_mismatch( 647 | "cookie1=value1; Path=/fo", 648 | "http://example.com/foo/bar", 649 | None, 650 | ); 651 | // match: cookie-path a prefix of request-path and first character of 652 | // request-path 653 | // not included in the cookie-path is / 654 | is_match( 655 | "cookie1=value1", 656 | "http://example.com/foo/", 657 | Some("http://example.com/foo/bar"), 658 | ); 659 | is_match( 660 | "cookie1=value1; Path=/foo", 661 | "http://example.com/foo/bar", 662 | None, 663 | ); 664 | // match: Path overridden to /, which matches all paths from the domain 665 | is_match( 666 | "cookie1=value1; Path=/", 667 | "http://example.com/foo/bar", 668 | Some("http://example.com/bus/baz"), 669 | ); 670 | // mismatch: different domain 671 | is_mismatch( 672 | "cookie1=value1", 673 | "http://example.com/foo/", 674 | Some("http://notmydomain.com/foo/bar"), 675 | ); 676 | is_mismatch( 677 | "cookie1=value1; Domain=example.com", 678 | "http://foo.example.com/foo/", 679 | Some("http://notmydomain.com/foo/bar"), 680 | ); 681 | // match: secure protocol 682 | is_match( 683 | "cookie1=value1; Secure", 684 | "http://example.com/foo/bar", 685 | Some("https://example.com/foo/bar"), 686 | ); 687 | // mismatch: non-secure protocol 688 | is_mismatch( 689 | "cookie1=value1; Secure", 690 | "http://example.com/foo/bar", 691 | Some("http://example.com/foo/bar"), 692 | ); 693 | // match: no http restriction 694 | is_match( 695 | "cookie1=value1", 696 | "http://example.com/foo/bar", 697 | Some("ftp://example.com/foo/bar"), 698 | ); 699 | // match: http protocol 700 | is_match( 701 | "cookie1=value1; HttpOnly", 702 | "http://example.com/foo/bar", 703 | Some("http://example.com/foo/bar"), 704 | ); 705 | is_match( 706 | "cookie1=value1; HttpOnly", 707 | "http://example.com/foo/bar", 708 | Some("HTTP://example.com/foo/bar"), 709 | ); 710 | is_match( 711 | "cookie1=value1; HttpOnly", 712 | "http://example.com/foo/bar", 713 | Some("https://example.com/foo/bar"), 714 | ); 715 | // mismatch: http requried 716 | is_mismatch( 717 | "cookie1=value1; HttpOnly", 718 | "http://example.com/foo/bar", 719 | Some("ftp://example.com/foo/bar"), 720 | ); 721 | is_mismatch( 722 | "cookie1=value1; HttpOnly", 723 | "http://example.com/foo/bar", 724 | Some("data:nonrelativescheme"), 725 | ); 726 | } 727 | } 728 | 729 | #[cfg(all(test, feature = "serde_json"))] 730 | mod serde_json_tests { 731 | use crate::cookie::Cookie; 732 | use crate::cookie_expiration::CookieExpiration; 733 | use crate::utils::test as test_utils; 734 | use crate::utils::test::*; 735 | use serde_json::json; 736 | use time; 737 | 738 | fn encode_decode(c: &Cookie<'_>, expected: serde_json::Value) { 739 | let encoded = serde_json::to_value(c).unwrap(); 740 | assert_eq!( 741 | expected, 742 | encoded, 743 | "\nexpected: '{}'\n encoded: '{}'", 744 | expected.to_string(), 745 | encoded.to_string() 746 | ); 747 | let decoded: Cookie<'_> = serde_json::from_value(encoded).unwrap(); 748 | assert_eq!( 749 | *c, 750 | decoded, 751 | "\nexpected: '{}'\n decoded: '{}'", 752 | c.to_string(), 753 | decoded.to_string() 754 | ); 755 | } 756 | 757 | #[test] 758 | fn serde() { 759 | encode_decode( 760 | &test_utils::make_cookie("cookie1=value1", "http://example.com/foo/bar", None, None), 761 | json!({ 762 | "raw_cookie": "cookie1=value1", 763 | "path": ["/foo", false], 764 | "domain": { "HostOnly": "example.com" }, 765 | "expires": "SessionEnd" 766 | }), 767 | ); 768 | 769 | encode_decode( 770 | &test_utils::make_cookie( 771 | "cookie2=value2; Domain=example.com", 772 | "http://foo.example.com/foo/bar", 773 | None, 774 | None, 775 | ), 776 | json!({ 777 | "raw_cookie": "cookie2=value2; Domain=example.com", 778 | "path": ["/foo", false], 779 | "domain": { "Suffix": "example.com" }, 780 | "expires": "SessionEnd" 781 | }), 782 | ); 783 | 784 | encode_decode( 785 | &test_utils::make_cookie( 786 | "cookie3=value3; Path=/foo/bar", 787 | "http://foo.example.com/foo", 788 | None, 789 | None, 790 | ), 791 | json!({ 792 | "raw_cookie": "cookie3=value3; Path=/foo/bar", 793 | "path": ["/foo/bar", true], 794 | "domain": { "HostOnly": "foo.example.com" }, 795 | "expires": "SessionEnd", 796 | }), 797 | ); 798 | 799 | let at_utc = time::macros::date!(2015 - 08 - 11) 800 | .with_time(time::macros::time!(16:41:42)) 801 | .assume_utc(); 802 | encode_decode( 803 | &test_utils::make_cookie( 804 | "cookie4=value4", 805 | "http://example.com/foo/bar", 806 | Some(at_utc), 807 | None, 808 | ), 809 | json!({ 810 | "raw_cookie": "cookie4=value4; Expires=Tue, 11 Aug 2015 16:41:42 GMT", 811 | "path": ["/foo", false], 812 | "domain": { "HostOnly": "example.com" }, 813 | "expires": { "AtUtc": at_utc.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, 814 | }), 815 | ); 816 | 817 | let expires = test_utils::make_cookie( 818 | "cookie5=value5", 819 | "http://example.com/foo/bar", 820 | Some(in_minutes(10)), 821 | None, 822 | ); 823 | let utc_tm = match expires.expires { 824 | CookieExpiration::AtUtc(ref utc_tm) => utc_tm, 825 | CookieExpiration::SessionEnd => unreachable!(), 826 | }; 827 | 828 | let utc_formatted = utc_tm 829 | .format(&time::format_description::well_known::Rfc2822) 830 | .unwrap() 831 | .to_string() 832 | .replace("+0000", "GMT"); 833 | let raw_cookie_value = format!("cookie5=value5; Expires={utc_formatted}"); 834 | 835 | encode_decode( 836 | &expires, 837 | json!({ 838 | "raw_cookie": raw_cookie_value, 839 | "path":["/foo", false], 840 | "domain": { "HostOnly": "example.com" }, 841 | "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, 842 | }), 843 | ); 844 | dbg!(&at_utc); 845 | let max_age = test_utils::make_cookie( 846 | "cookie6=value6", 847 | "http://example.com/foo/bar", 848 | Some(at_utc), 849 | Some(10), 850 | ); 851 | dbg!(&max_age); 852 | let utc_tm = match max_age.expires { 853 | CookieExpiration::AtUtc(ref utc_tm) => time::OffsetDateTime::parse( 854 | &utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap(), 855 | &time::format_description::well_known::Rfc3339, 856 | ) 857 | .expect("could not re-parse time"), 858 | CookieExpiration::SessionEnd => unreachable!(), 859 | }; 860 | dbg!(&utc_tm); 861 | encode_decode( 862 | &max_age, 863 | json!({ 864 | "raw_cookie": "cookie6=value6; Max-Age=10; Expires=Tue, 11 Aug 2015 16:41:42 GMT", 865 | "path":["/foo", false], 866 | "domain": { "HostOnly": "example.com" }, 867 | "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, 868 | }), 869 | ); 870 | 871 | let max_age = test_utils::make_cookie( 872 | "cookie7=value7", 873 | "http://example.com/foo/bar", 874 | None, 875 | Some(10), 876 | ); 877 | let utc_tm = match max_age.expires { 878 | CookieExpiration::AtUtc(ref utc_tm) => utc_tm, 879 | CookieExpiration::SessionEnd => unreachable!(), 880 | }; 881 | encode_decode( 882 | &max_age, 883 | json!({ 884 | "raw_cookie": "cookie7=value7; Max-Age=10", 885 | "path":["/foo", false], 886 | "domain": { "HostOnly": "example.com" }, 887 | "expires": { "AtUtc": utc_tm.format(crate::rfc3339_fmt::RFC3339_FORMAT).unwrap().to_string() }, 888 | }), 889 | ); 890 | } 891 | } 892 | -------------------------------------------------------------------------------- /src/cookie_domain.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | use cookie::Cookie as RawCookie; 4 | use idna; 5 | #[cfg(feature = "public_suffix")] 6 | use publicsuffix::{List, Psl, Suffix}; 7 | #[cfg(feature = "serde")] 8 | use serde_derive::{Deserialize, Serialize}; 9 | use std::convert::TryFrom; 10 | use url::{Host, Url}; 11 | 12 | use crate::utils::is_host_name; 13 | use crate::CookieError; 14 | 15 | pub fn is_match(domain: &str, request_url: &Url) -> bool { 16 | CookieDomain::try_from(domain) 17 | .map(|domain| domain.matches(request_url)) 18 | .unwrap_or(false) 19 | } 20 | 21 | /// The domain of a `Cookie` 22 | #[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)] 23 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 24 | pub enum CookieDomain { 25 | /// No Domain attribute in Set-Cookie header 26 | HostOnly(String), 27 | /// Domain attribute from Set-Cookie header 28 | Suffix(String), 29 | /// Domain attribute was not present in the Set-Cookie header 30 | NotPresent, 31 | /// Domain attribute-value was empty; technically undefined behavior, but suggested that this 32 | /// be treated as invalid 33 | Empty, 34 | } 35 | 36 | // 5.1.3. Domain Matching 37 | // A string domain-matches a given domain string if at least one of the 38 | // following conditions hold: 39 | // 40 | // o The domain string and the string are identical. (Note that both 41 | // the domain string and the string will have been canonicalized to 42 | // lower case at this point.) 43 | // 44 | // o All of the following conditions hold: 45 | // 46 | // * The domain string is a suffix of the string. 47 | // 48 | // * The last character of the string that is not included in the 49 | // domain string is a %x2E (".") character. 50 | // 51 | // * The string is a host name (i.e., not an IP address). 52 | /// The concept of a domain match per [IETF RFC6265 Section 53 | /// 5.1.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3) 54 | impl CookieDomain { 55 | /// Get the CookieDomain::HostOnly variant based on `request_url`. This is the effective behavior of 56 | /// setting the domain-attribute to empty 57 | pub fn host_only(request_url: &Url) -> Result { 58 | request_url 59 | .host() 60 | .ok_or(CookieError::NonRelativeScheme) 61 | .map(|h| match h { 62 | Host::Domain(d) => CookieDomain::HostOnly(d.into()), 63 | Host::Ipv4(addr) => CookieDomain::HostOnly(format!("{}", addr)), 64 | Host::Ipv6(addr) => CookieDomain::HostOnly(format!("[{}]", addr)), 65 | }) 66 | } 67 | 68 | /// Tests if the given `url::Url` meets the domain-match criteria 69 | pub fn matches(&self, request_url: &Url) -> bool { 70 | if let Some(url_host) = request_url.host_str() { 71 | match *self { 72 | CookieDomain::HostOnly(ref host) => host == url_host, 73 | CookieDomain::Suffix(ref suffix) => { 74 | suffix == url_host 75 | || (is_host_name(url_host) 76 | && url_host.ends_with(suffix) 77 | && url_host[(url_host.len() - suffix.len() - 1)..].starts_with('.')) 78 | } 79 | CookieDomain::NotPresent | CookieDomain::Empty => false, // nothing can match the Empty case 80 | } 81 | } else { 82 | false // not a matchable scheme 83 | } 84 | } 85 | 86 | /// Tests if the given `url::Url` has a request-host identical to the domain attribute 87 | pub fn host_is_identical(&self, request_url: &Url) -> bool { 88 | if let Some(url_host) = request_url.host_str() { 89 | match *self { 90 | CookieDomain::HostOnly(ref host) => host == url_host, 91 | CookieDomain::Suffix(ref suffix) => suffix == url_host, 92 | CookieDomain::NotPresent | CookieDomain::Empty => false, // nothing can match the Empty case 93 | } 94 | } else { 95 | false // not a matchable scheme 96 | } 97 | } 98 | 99 | /// Tests if the domain-attribute is a public suffix as indicated by the provided 100 | /// `publicsuffix::List`. 101 | #[cfg(feature = "public_suffix")] 102 | pub fn is_public_suffix(&self, psl: &List) -> bool { 103 | if let Some(domain) = self.as_cow().as_ref().map(|d| d.as_bytes()) { 104 | psl.suffix(domain) 105 | // Only consider suffixes explicitly listed in the public suffix list 106 | // to avoid issues like https://github.com/curl/curl/issues/658 107 | .filter(Suffix::is_known) 108 | .filter(|suffix| suffix == &domain) 109 | .is_some() 110 | } else { 111 | false 112 | } 113 | } 114 | 115 | /// Get a borrowed string representation of the domain. For `Empty` and `NotPresent` variants, 116 | /// `None` shall be returned; 117 | pub fn as_cow(&self) -> Option> { 118 | match *self { 119 | CookieDomain::HostOnly(ref s) | CookieDomain::Suffix(ref s) => { 120 | Some(std::borrow::Cow::Borrowed(s)) 121 | } 122 | CookieDomain::Empty | CookieDomain::NotPresent => None, 123 | } 124 | } 125 | } 126 | 127 | /// Construct a `CookieDomain::Suffix` from a string, stripping a single leading '.' if present. 128 | /// If the source string is empty, returns the `CookieDomain::Empty` variant. 129 | impl<'a> TryFrom<&'a str> for CookieDomain { 130 | type Error = crate::Error; 131 | fn try_from(value: &str) -> Result { 132 | idna::domain_to_ascii(value.trim()) 133 | .map_err(super::IdnaErrors::from) 134 | .map_err(Into::into) 135 | .map(|domain| { 136 | if domain.is_empty() || "." == domain { 137 | CookieDomain::Empty 138 | } else if domain.starts_with('.') { 139 | CookieDomain::Suffix(String::from(&domain[1..])) 140 | } else { 141 | CookieDomain::Suffix(domain) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | /// Construct a `CookieDomain::Suffix` from a `cookie::Cookie`, which handles stripping a leading 148 | /// '.' for us. If the cookie.domain is None or an empty string, the `CookieDomain::Empty` variant 149 | /// is returned. 150 | /// __NOTE__: `cookie::Cookie` domain values already have the leading '.' stripped. To avoid 151 | /// performing this step twice, the `From<&cookie::Cookie>` impl should be used, 152 | /// instead of passing `cookie.domain` to the `From<&str>` impl. 153 | impl<'a, 'c> TryFrom<&'a RawCookie<'c>> for CookieDomain { 154 | type Error = crate::Error; 155 | fn try_from(cookie: &'a RawCookie<'c>) -> Result { 156 | if let Some(domain) = cookie.domain() { 157 | idna::domain_to_ascii(domain.trim()) 158 | .map_err(super::IdnaErrors::from) 159 | .map_err(Into::into) 160 | .map(|domain| { 161 | if domain.is_empty() { 162 | CookieDomain::Empty 163 | } else { 164 | CookieDomain::Suffix(domain) 165 | } 166 | }) 167 | } else { 168 | Ok(CookieDomain::NotPresent) 169 | } 170 | } 171 | } 172 | 173 | impl<'a> From<&'a CookieDomain> for String { 174 | fn from(c: &'a CookieDomain) -> String { 175 | match *c { 176 | CookieDomain::HostOnly(ref h) => h.to_owned(), 177 | CookieDomain::Suffix(ref s) => s.to_owned(), 178 | CookieDomain::Empty | CookieDomain::NotPresent => "".to_owned(), 179 | } 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use cookie::Cookie as RawCookie; 186 | use std::convert::TryFrom; 187 | use url::Url; 188 | 189 | use super::CookieDomain; 190 | use crate::utils::test::*; 191 | 192 | #[inline] 193 | fn matches(expected: bool, cookie_domain: &CookieDomain, url: &str) { 194 | let url = Url::parse(url).unwrap(); 195 | assert!( 196 | expected == cookie_domain.matches(&url), 197 | "cookie_domain: {:?} url: {:?}, url.host_str(): {:?}", 198 | cookie_domain, 199 | url, 200 | url.host_str() 201 | ); 202 | } 203 | 204 | #[inline] 205 | fn variants(expected: bool, cookie_domain: &CookieDomain, url: &str) { 206 | matches(expected, cookie_domain, url); 207 | matches(expected, cookie_domain, &format!("{}/", url)); 208 | matches(expected, cookie_domain, &format!("{}:8080", url)); 209 | matches(expected, cookie_domain, &format!("{}/foo/bar", url)); 210 | matches(expected, cookie_domain, &format!("{}:8080/foo/bar", url)); 211 | } 212 | 213 | #[test] 214 | fn matches_hostonly() { 215 | { 216 | let url = url("http://example.com"); 217 | // HostOnly must be an identical string match, and may be an IP address 218 | // or a hostname 219 | let host_name = CookieDomain::host_only(&url).expect("unable to parse domain"); 220 | matches(false, &host_name, "data:nonrelative"); 221 | variants(true, &host_name, "http://example.com"); 222 | variants(false, &host_name, "http://example.org"); 223 | // per RFC6265: 224 | // WARNING: Some existing user agents treat an absent Domain 225 | // attribute as if the Domain attribute were present and contained 226 | // the current host name. For example, if example.com returns a Set- 227 | // Cookie header without a Domain attribute, these user agents will 228 | // erroneously send the cookie to www.example.com as well. 229 | variants(false, &host_name, "http://foo.example.com"); 230 | variants(false, &host_name, "http://127.0.0.1"); 231 | variants(false, &host_name, "http://[::1]"); 232 | } 233 | 234 | { 235 | let url = url("http://127.0.0.1"); 236 | let ip4 = CookieDomain::host_only(&url).expect("unable to parse Ipv4"); 237 | matches(false, &ip4, "data:nonrelative"); 238 | variants(true, &ip4, "http://127.0.0.1"); 239 | variants(false, &ip4, "http://[::1]"); 240 | } 241 | 242 | { 243 | let url = url("http://[::1]"); 244 | let ip6 = CookieDomain::host_only(&url).expect("unable to parse Ipv6"); 245 | matches(false, &ip6, "data:nonrelative"); 246 | variants(false, &ip6, "http://127.0.0.1"); 247 | variants(true, &ip6, "http://[::1]"); 248 | } 249 | } 250 | 251 | #[test] 252 | fn from_strs() { 253 | assert_eq!( 254 | CookieDomain::Empty, 255 | CookieDomain::try_from("").expect("unable to parse domain") 256 | ); 257 | assert_eq!( 258 | CookieDomain::Empty, 259 | CookieDomain::try_from(".").expect("unable to parse domain") 260 | ); 261 | // per [IETF RFC6265 Section 5.2.3](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3) 262 | //If the first character of the attribute-value string is %x2E ("."): 263 | // 264 | //Let cookie-domain be the attribute-value without the leading %x2E 265 | //(".") character. 266 | assert_eq!( 267 | CookieDomain::Suffix(String::from(".")), 268 | CookieDomain::try_from("..").expect("unable to parse domain") 269 | ); 270 | assert_eq!( 271 | CookieDomain::Suffix(String::from("example.com")), 272 | CookieDomain::try_from("example.com").expect("unable to parse domain") 273 | ); 274 | assert_eq!( 275 | CookieDomain::Suffix(String::from("example.com")), 276 | CookieDomain::try_from(".example.com").expect("unable to parse domain") 277 | ); 278 | assert_eq!( 279 | CookieDomain::Suffix(String::from(".example.com")), 280 | CookieDomain::try_from("..example.com").expect("unable to parse domain") 281 | ); 282 | } 283 | 284 | #[test] 285 | fn from_raw_cookie() { 286 | fn raw_cookie(s: &str) -> RawCookie<'_> { 287 | RawCookie::parse(s).unwrap() 288 | } 289 | assert_eq!( 290 | CookieDomain::NotPresent, 291 | CookieDomain::try_from(&raw_cookie("cookie=value")).expect("unable to parse domain") 292 | ); 293 | // cookie::Cookie handles this (cookie.domain == None) 294 | assert_eq!( 295 | CookieDomain::NotPresent, 296 | CookieDomain::try_from(&raw_cookie("cookie=value; Domain=")) 297 | .expect("unable to parse domain") 298 | ); 299 | // cookie::Cookie does not handle this (empty after stripping leading dot) 300 | assert_eq!( 301 | CookieDomain::Empty, 302 | CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.")) 303 | .expect("unable to parse domain") 304 | ); 305 | assert_eq!( 306 | CookieDomain::Suffix(String::from("example.com")), 307 | CookieDomain::try_from(&raw_cookie("cookie=value; Domain=.example.com")) 308 | .expect("unable to parse domain") 309 | ); 310 | assert_eq!( 311 | CookieDomain::Suffix(String::from("example.com")), 312 | CookieDomain::try_from(&raw_cookie("cookie=value; Domain=example.com")) 313 | .expect("unable to parse domain") 314 | ); 315 | } 316 | 317 | #[test] 318 | fn matches_suffix() { 319 | { 320 | let suffix = CookieDomain::try_from("example.com").expect("unable to parse domain"); 321 | variants(true, &suffix, "http://example.com"); // exact match 322 | variants(true, &suffix, "http://foo.example.com"); // suffix match 323 | variants(false, &suffix, "http://example.org"); // no match 324 | variants(false, &suffix, "http://xample.com"); // request is the suffix, no match 325 | variants(false, &suffix, "http://fooexample.com"); // suffix, but no "." b/w foo and example, no match 326 | } 327 | 328 | { 329 | // strip leading dot 330 | let suffix = CookieDomain::try_from(".example.com").expect("unable to parse domain"); 331 | variants(true, &suffix, "http://example.com"); 332 | variants(true, &suffix, "http://foo.example.com"); 333 | variants(false, &suffix, "http://example.org"); 334 | variants(false, &suffix, "http://xample.com"); 335 | variants(false, &suffix, "http://fooexample.com"); 336 | } 337 | 338 | { 339 | // only first leading dot is stripped 340 | let suffix = CookieDomain::try_from("..example.com").expect("unable to parse domain"); 341 | variants(true, &suffix, "http://.example.com"); 342 | variants(true, &suffix, "http://foo..example.com"); 343 | variants(false, &suffix, "http://example.com"); 344 | variants(false, &suffix, "http://foo.example.com"); 345 | variants(false, &suffix, "http://example.org"); 346 | variants(false, &suffix, "http://xample.com"); 347 | variants(false, &suffix, "http://fooexample.com"); 348 | } 349 | 350 | { 351 | // an exact string match, although an IP is specified 352 | let suffix = CookieDomain::try_from("127.0.0.1").expect("unable to parse Ipv4"); 353 | variants(true, &suffix, "http://127.0.0.1"); 354 | } 355 | 356 | { 357 | // an exact string match, although an IP is specified 358 | let suffix = CookieDomain::try_from("[::1]").expect("unable to parse Ipv6"); 359 | variants(true, &suffix, "http://[::1]"); 360 | } 361 | 362 | { 363 | // non-identical suffix match only works for host names (i.e. not IPs) 364 | let suffix = CookieDomain::try_from("0.0.1").expect("unable to parse Ipv4"); 365 | variants(false, &suffix, "http://127.0.0.1"); 366 | } 367 | } 368 | } 369 | 370 | #[cfg(all(test, feature = "serde_json"))] 371 | mod serde_json_tests { 372 | use serde_json; 373 | use std::convert::TryFrom; 374 | 375 | use crate::cookie_domain::CookieDomain; 376 | use crate::utils::test::*; 377 | 378 | fn encode_decode(cd: &CookieDomain, exp_json: &str) { 379 | let encoded = serde_json::to_string(cd).unwrap(); 380 | assert!( 381 | exp_json == encoded, 382 | "expected: '{}'\n encoded: '{}'", 383 | exp_json, 384 | encoded 385 | ); 386 | let decoded: CookieDomain = serde_json::from_str(&encoded).unwrap(); 387 | assert!( 388 | *cd == decoded, 389 | "expected: '{:?}'\n decoded: '{:?}'", 390 | cd, 391 | decoded 392 | ); 393 | } 394 | 395 | #[test] 396 | fn serde() { 397 | let url = url("http://example.com"); 398 | encode_decode( 399 | &CookieDomain::host_only(&url).expect("cannot parse domain"), 400 | "{\"HostOnly\":\"example.com\"}", 401 | ); 402 | encode_decode( 403 | &CookieDomain::try_from(".example.com").expect("cannot parse domain"), 404 | "{\"Suffix\":\"example.com\"}", 405 | ); 406 | encode_decode(&CookieDomain::NotPresent, "\"NotPresent\""); 407 | encode_decode(&CookieDomain::Empty, "\"Empty\""); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/cookie_expiration.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde_derive::{Deserialize, Serialize}; 5 | use time::{self, OffsetDateTime}; 6 | 7 | /// When a given `Cookie` expires 8 | #[derive(Eq, Clone, Debug)] 9 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 10 | pub enum CookieExpiration { 11 | /// `Cookie` expires at the given UTC time, as set from either the Max-Age 12 | /// or Expires attribute of a Set-Cookie header 13 | #[cfg_attr(feature = "serde", serde(with = "crate::rfc3339_fmt"))] 14 | AtUtc(OffsetDateTime), 15 | /// `Cookie` expires at the end of the current `Session`; this means the cookie 16 | /// is not persistent 17 | SessionEnd, 18 | } 19 | 20 | // We directly impl `PartialEq` as the cookie Expires attribute does not include nanosecond precision 21 | impl std::cmp::PartialEq for CookieExpiration { 22 | fn eq(&self, other: &Self) -> bool { 23 | match (self, other) { 24 | (CookieExpiration::SessionEnd, CookieExpiration::SessionEnd) => true, 25 | (CookieExpiration::AtUtc(this_offset), CookieExpiration::AtUtc(other_offset)) => { 26 | // All instances should already be UTC offset 27 | this_offset.date() == other_offset.date() 28 | && this_offset.time().hour() == other_offset.time().hour() 29 | && this_offset.time().minute() == other_offset.time().minute() 30 | && this_offset.time().second() == other_offset.time().second() 31 | } 32 | _ => false, 33 | } 34 | } 35 | } 36 | 37 | impl CookieExpiration { 38 | /// Indicates if the `Cookie` is expired as of *now*. 39 | pub fn is_expired(&self) -> bool { 40 | self.expires_by(&time::OffsetDateTime::now_utc()) 41 | } 42 | 43 | /// Indicates if the `Cookie` expires as of `utc_tm`. 44 | pub fn expires_by(&self, utc_tm: &time::OffsetDateTime) -> bool { 45 | match *self { 46 | CookieExpiration::AtUtc(ref expire_tm) => *expire_tm <= *utc_tm, 47 | CookieExpiration::SessionEnd => false, 48 | } 49 | } 50 | } 51 | 52 | const MAX_RFC3339: time::OffsetDateTime = time::macros::date!(9999 - 12 - 31) 53 | .with_time(time::macros::time!(23:59:59)) 54 | .assume_utc(); 55 | impl From for CookieExpiration { 56 | fn from(max_age: u64) -> CookieExpiration { 57 | // make sure we don't trigger a panic! in Duration by restricting the seconds 58 | // to the max 59 | CookieExpiration::from(time::Duration::seconds(std::cmp::min( 60 | time::Duration::MAX.whole_seconds() as u64, 61 | max_age, 62 | ) as i64)) 63 | } 64 | } 65 | 66 | impl From for CookieExpiration { 67 | fn from(utc_tm: OffsetDateTime) -> CookieExpiration { 68 | CookieExpiration::AtUtc(utc_tm.min(MAX_RFC3339)) 69 | } 70 | } 71 | 72 | impl From for CookieExpiration { 73 | fn from(expiration: cookie::Expiration) -> CookieExpiration { 74 | match expiration { 75 | cookie::Expiration::DateTime(offset) => CookieExpiration::AtUtc(offset), 76 | cookie::Expiration::Session => CookieExpiration::SessionEnd, 77 | } 78 | } 79 | } 80 | 81 | impl From for CookieExpiration { 82 | fn from(duration: time::Duration) -> Self { 83 | // If delta-seconds is less than or equal to zero (0), let expiry-time 84 | // be the earliest representable date and time. Otherwise, let the 85 | // expiry-time be the current date and time plus delta-seconds seconds. 86 | let utc_tm = if duration.is_zero() { 87 | time::OffsetDateTime::UNIX_EPOCH 88 | } else { 89 | let now_utc = time::OffsetDateTime::now_utc(); 90 | let d = (MAX_RFC3339 - now_utc).min(duration); 91 | now_utc + d 92 | }; 93 | CookieExpiration::from(utc_tm) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::CookieExpiration; 100 | use time; 101 | 102 | use crate::utils::test::*; 103 | 104 | #[test] 105 | fn max_age_bounds() { 106 | match CookieExpiration::from(time::Duration::MAX.whole_seconds() as u64 + 1) { 107 | CookieExpiration::AtUtc(_) => assert!(true), 108 | _ => assert!(false), 109 | } 110 | } 111 | 112 | #[test] 113 | fn expired() { 114 | let ma = CookieExpiration::from(0u64); // Max-Age<=0 indicates the cookie is expired 115 | assert!(ma.is_expired()); 116 | assert!(ma.expires_by(&in_days(-1))); 117 | } 118 | 119 | #[test] 120 | fn max_age() { 121 | let ma = CookieExpiration::from(60u64); 122 | assert!(!ma.is_expired()); 123 | assert!(ma.expires_by(&in_minutes(2))); 124 | } 125 | 126 | #[test] 127 | fn session_end() { 128 | // SessionEnd never "expires"; lives until end of session 129 | let se = CookieExpiration::SessionEnd; 130 | assert!(!se.is_expired()); 131 | assert!(!se.expires_by(&in_days(1))); 132 | assert!(!se.expires_by(&in_days(-1))); 133 | } 134 | 135 | #[test] 136 | fn at_utc() { 137 | { 138 | let expire_tmrw = CookieExpiration::from(in_days(1)); 139 | assert!(!expire_tmrw.is_expired()); 140 | assert!(expire_tmrw.expires_by(&in_days(2))); 141 | } 142 | { 143 | let expired_yest = CookieExpiration::from(in_days(-1)); 144 | assert!(expired_yest.is_expired()); 145 | assert!(!expired_yest.expires_by(&in_days(-2))); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/cookie_path.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde_derive::{Deserialize, Serialize}; 3 | use std::cmp::max; 4 | use std::ops::Deref; 5 | use url::Url; 6 | 7 | /// Returns true if `request_url` path-matches `path` per 8 | /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) 9 | pub fn is_match(path: &str, request_url: &Url) -> bool { 10 | CookiePath::parse(path).map_or(false, |cp| cp.matches(request_url)) 11 | } 12 | 13 | /// The path of a `Cookie` 14 | #[derive(PartialEq, Eq, Clone, Debug, Hash, PartialOrd, Ord)] 15 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 16 | pub struct CookiePath(String, bool); 17 | impl CookiePath { 18 | /// Determine if `request_url` path-matches this `CookiePath` per 19 | /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) 20 | pub fn matches(&self, request_url: &Url) -> bool { 21 | if request_url.cannot_be_a_base() { 22 | false 23 | } else { 24 | let request_path = request_url.path(); 25 | let cookie_path = &*self.0; 26 | // o The cookie-path and the request-path are identical. 27 | cookie_path == request_path 28 | || (request_path.starts_with(cookie_path) 29 | && (cookie_path.ends_with('/') 30 | || &request_path[cookie_path.len()..=cookie_path.len()] == "/")) 31 | } 32 | } 33 | 34 | /// Returns true if this `CookiePath` was set from a Path attribute; this allows us to 35 | /// distinguish from the case where Path was explicitly set to "/" 36 | pub fn is_from_path_attr(&self) -> bool { 37 | self.1 38 | } 39 | 40 | // The user agent MUST use an algorithm equivalent to the following 41 | // algorithm to compute the default-path of a cookie: 42 | // 43 | // 1. Let uri-path be the path portion of the request-uri if such a 44 | // portion exists (and empty otherwise). For example, if the 45 | // request-uri contains just a path (and optional query string), 46 | // then the uri-path is that path (without the %x3F ("?") character 47 | // or query string), and if the request-uri contains a full 48 | // absoluteURI, the uri-path is the path component of that URI. 49 | // 50 | // 2. If the uri-path is empty or if the first character of the uri- 51 | // path is not a %x2F ("/") character, output %x2F ("/") and skip 52 | // the remaining steps. 53 | // 54 | // 3. If the uri-path contains no more than one %x2F ("/") character, 55 | // output %x2F ("/") and skip the remaining step. 56 | // 57 | // 4. Output the characters of the uri-path from the first character up 58 | // to, but not including, the right-most %x2F ("/"). 59 | /// Determine the default-path of `request_url` per 60 | /// [IETF RFC6265 Section 5.1.4](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4) 61 | pub fn default_path(request_url: &Url) -> CookiePath { 62 | let cp = if request_url.cannot_be_a_base() { 63 | // non-relative path scheme, default to "/" (uri-path "empty", case 2) 64 | "/".into() 65 | } else { 66 | let path = request_url.path(); 67 | match path.rfind('/') { 68 | None => "/".into(), // no "/" in string, default to "/" (case 2) 69 | Some(i) => path[0..max(i, 1)].into(), // case 4 (subsumes case 3) 70 | } 71 | }; 72 | CookiePath(cp, false) 73 | } 74 | 75 | /// Attempt to parse `path` as a `CookiePath`; if unsuccessful, the default-path of 76 | /// `request_url` will be returned as the `CookiePath`. 77 | pub fn new(path: &str, request_url: &Url) -> CookiePath { 78 | match CookiePath::parse(path) { 79 | Some(cp) => cp, 80 | None => CookiePath::default_path(request_url), 81 | } 82 | } 83 | 84 | /// Attempt to parse `path` as a `CookiePath`. If `path` does not have a leading "/", 85 | /// `None` is returned. 86 | pub fn parse(path: &str) -> Option { 87 | if path.starts_with('/') { 88 | Some(CookiePath(String::from(path), true)) 89 | } else { 90 | None 91 | } 92 | } 93 | } 94 | 95 | impl AsRef for CookiePath { 96 | fn as_ref(&self) -> &str { 97 | &self.0 98 | } 99 | } 100 | 101 | impl Deref for CookiePath { 102 | type Target = str; 103 | fn deref(&self) -> &Self::Target { 104 | &self.0 105 | } 106 | } 107 | 108 | impl<'a> From<&'a CookiePath> for String { 109 | fn from(cp: &CookiePath) -> String { 110 | cp.0.clone() 111 | } 112 | } 113 | 114 | impl From for String { 115 | fn from(cp: CookiePath) -> String { 116 | cp.0 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::CookiePath; 123 | use url::Url; 124 | 125 | #[test] 126 | fn default_path() { 127 | fn get_path(url: &str) -> String { 128 | CookiePath::default_path(&Url::parse(url).expect("unable to parse url in default_path")) 129 | .into() 130 | } 131 | assert_eq!(get_path("data:foobusbar"), "/"); 132 | assert_eq!(get_path("http://example.com"), "/"); 133 | assert_eq!(get_path("http://example.com/"), "/"); 134 | assert_eq!(get_path("http://example.com/foo"), "/"); 135 | assert_eq!(get_path("http://example.com/foo/"), "/foo"); 136 | assert_eq!(get_path("http://example.com//foo/"), "//foo"); 137 | assert_eq!(get_path("http://example.com/foo//"), "/foo/"); 138 | assert_eq!(get_path("http://example.com/foo/bus/bar"), "/foo/bus"); 139 | assert_eq!(get_path("http://example.com/foo//bus/bar"), "/foo//bus"); 140 | assert_eq!(get_path("http://example.com/foo/bus/bar/"), "/foo/bus/bar"); 141 | } 142 | 143 | fn do_match(exp: bool, cp: &str, rp: &str) { 144 | let url = Url::parse(&format!("http://example.com{}", rp)) 145 | .expect("unable to parse url in do_match"); 146 | let cp = CookiePath::parse(cp).expect("unable to parse CookiePath in do_match"); 147 | assert!( 148 | exp == cp.matches(&url), 149 | "\n>> {:?}\nshould{}match\n>> {:?}\n>> {:?}\n", 150 | cp, 151 | if exp { " " } else { " NOT " }, 152 | url, 153 | url.path() 154 | ); 155 | } 156 | fn is_match(cp: &str, rp: &str) { 157 | do_match(true, cp, rp); 158 | } 159 | fn is_mismatch(cp: &str, rp: &str) { 160 | do_match(false, cp, rp); 161 | } 162 | 163 | #[test] 164 | fn bad_paths() { 165 | assert!(CookiePath::parse("").is_none()); 166 | assert!(CookiePath::parse("a/foo").is_none()); 167 | } 168 | 169 | #[test] 170 | fn bad_path_defaults() { 171 | fn get_path(cp: &str, url: &str) -> String { 172 | CookiePath::new( 173 | cp, 174 | &Url::parse(url).expect("unable to parse url in bad_path_defaults"), 175 | ) 176 | .into() 177 | } 178 | assert_eq!(get_path("", "http://example.com/"), "/"); 179 | assert_eq!(get_path("a/foo", "http://example.com/"), "/"); 180 | assert_eq!(get_path("", "http://example.com/foo/bar"), "/foo"); 181 | assert_eq!(get_path("a/foo", "http://example.com/foo/bar"), "/foo"); 182 | assert_eq!(get_path("", "http://example.com/foo/bar/"), "/foo/bar"); 183 | assert_eq!(get_path("a/foo", "http://example.com/foo/bar/"), "/foo/bar"); 184 | } 185 | 186 | #[test] 187 | fn shortest_path() { 188 | is_match("/", "/"); 189 | } 190 | 191 | // A request-path path-matches a given cookie-path if at least one of 192 | // the following conditions holds: 193 | #[test] 194 | fn identical_paths() { 195 | // o The cookie-path and the request-path are identical. 196 | is_match("/foo/bus", "/foo/bus"); // identical 197 | is_mismatch("/foo/bus", "/foo/buss"); // trailing character 198 | is_mismatch("/foo/bus", "/zoo/bus"); // character mismatch 199 | is_mismatch("/foo/bus", "/zfoo/bus"); // leading character 200 | } 201 | 202 | #[test] 203 | fn cookie_path_prefix1() { 204 | // o The cookie-path is a prefix of the request-path, and the last 205 | // character of the cookie-path is %x2F ("/"). 206 | is_match("/foo/", "/foo/bus"); // cookie-path a prefix and ends in "/" 207 | is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path 208 | is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path 209 | is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but last char != "/" and first char in request-path ("o") after prefix != "/" 210 | } 211 | 212 | #[test] 213 | fn cookie_path_prefix2() { 214 | // o The cookie-path is a prefix of the request-path, and the first 215 | // character of the request-path that is not included in the cookie- 216 | // path is a %x2F ("/") character. 217 | is_match("/foo", "/foo/bus"); // cookie-path a prefix of request-path, and next char in request-path = "/" 218 | is_mismatch("/bar", "/foo/bus"); // cookie-path not a prefix of request-path 219 | is_mismatch("/foo/bus/bar", "/foo/bus"); // cookie-path not a prefix of request-path 220 | is_mismatch("/fo", "/foo/bus"); // cookie-path a prefix, but next char in request-path ("o") != "/" 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/cookie_store.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, Write}; 2 | use std::ops::Deref; 3 | 4 | use cookie::Cookie as RawCookie; 5 | use log::debug; 6 | use url::Url; 7 | 8 | use crate::cookie::Cookie; 9 | use crate::cookie_domain::is_match as domain_match; 10 | use crate::cookie_path::is_match as path_match; 11 | use crate::utils::{is_http_scheme, is_secure}; 12 | use crate::CookieError; 13 | 14 | #[cfg(feature = "preserve_order")] 15 | use indexmap::IndexMap; 16 | #[cfg(not(feature = "preserve_order"))] 17 | use std::collections::HashMap; 18 | #[cfg(feature = "preserve_order")] 19 | type Map = IndexMap; 20 | #[cfg(not(feature = "preserve_order"))] 21 | type Map = HashMap; 22 | 23 | type NameMap = Map>; 24 | type PathMap = Map; 25 | type DomainMap = Map; 26 | 27 | #[derive(PartialEq, Clone, Debug, Eq)] 28 | pub enum StoreAction { 29 | /// The `Cookie` was successfully added to the store 30 | Inserted, 31 | /// The `Cookie` successfully expired a `Cookie` already in the store 32 | ExpiredExisting, 33 | /// The `Cookie` was added to the store, replacing an existing entry 34 | UpdatedExisting, 35 | } 36 | 37 | pub type StoreResult = Result; 38 | pub type InsertResult = Result; 39 | 40 | #[derive(Debug, Default, Clone)] 41 | /// An implementation for storing and retrieving [`Cookie`]s per the path and domain matching 42 | /// rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). 43 | pub struct CookieStore { 44 | /// Cookies stored by domain, path, then name 45 | cookies: DomainMap, 46 | #[cfg(feature = "public_suffix")] 47 | /// If set, enables [public suffix](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) rejection based on the provided `publicsuffix::List` 48 | public_suffix_list: Option, 49 | } 50 | 51 | impl CookieStore { 52 | #[deprecated( 53 | since = "0.14.1", 54 | note = "Please use the `get_request_values` function instead" 55 | )] 56 | /// Return an `Iterator` of the cookies for `url` in the store, suitable for submitting in an 57 | /// HTTP request. As the items are intended for use in creating a `Cookie` header in a GET request, 58 | /// they may contain only the `name` and `value` of a received cookie, eliding other parameters 59 | /// such as `path` or `expires`. For iteration over `Cookie` instances containing all data, please 60 | /// refer to [`CookieStore::matches`]. 61 | pub fn get_request_cookies(&self, url: &Url) -> impl Iterator> { 62 | self.matches(url).into_iter().map(|c| c.deref()) 63 | } 64 | 65 | /// Return an `Iterator` of the cookie (`name`, `value`) pairs for `url` in the store, suitable 66 | /// for use in the `Cookie` header of an HTTP request. For iteration over `Cookie` instances, 67 | /// please refer to [`CookieStore::matches`]. 68 | pub fn get_request_values(&self, url: &Url) -> impl Iterator { 69 | self.matches(url).into_iter().map(|c| c.name_value()) 70 | } 71 | 72 | /// Store the `cookies` received from `url` 73 | pub fn store_response_cookies>>( 74 | &mut self, 75 | cookies: I, 76 | url: &Url, 77 | ) { 78 | for cookie in cookies { 79 | if cookie.secure() != Some(true) || cfg!(feature = "log_secure_cookie_values") { 80 | debug!("inserting Set-Cookie '{:?}'", cookie); 81 | } else { 82 | debug!("inserting secure cookie '{}'", cookie.name()); 83 | } 84 | 85 | if let Err(e) = self.insert_raw(&cookie, url) { 86 | debug!("unable to store Set-Cookie: {:?}", e); 87 | } 88 | } 89 | } 90 | 91 | /// Specify a `publicsuffix::List` for the `CookieStore` to allow [public suffix 92 | /// matching](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) 93 | #[cfg(feature = "public_suffix")] 94 | pub fn with_suffix_list(self, psl: publicsuffix::List) -> CookieStore { 95 | CookieStore { 96 | cookies: self.cookies, 97 | public_suffix_list: Some(psl), 98 | } 99 | } 100 | 101 | /// Returns true if the `CookieStore` contains an __unexpired__ `Cookie` corresponding to the 102 | /// specified `domain`, `path`, and `name`. 103 | pub fn contains(&self, domain: &str, path: &str, name: &str) -> bool { 104 | self.get(domain, path, name).is_some() 105 | } 106 | 107 | /// Returns true if the `CookieStore` contains any (even an __expired__) `Cookie` corresponding 108 | /// to the specified `domain`, `path`, and `name`. 109 | pub fn contains_any(&self, domain: &str, path: &str, name: &str) -> bool { 110 | self.get_any(domain, path, name).is_some() 111 | } 112 | 113 | /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`, 114 | /// `path`, and `name`. 115 | pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'_>> { 116 | self.get_any(domain, path, name).and_then(|cookie| { 117 | if cookie.is_expired() { 118 | None 119 | } else { 120 | Some(cookie) 121 | } 122 | }) 123 | } 124 | 125 | /// Returns a mutable reference to the __unexpired__ `Cookie` corresponding to the specified 126 | /// `domain`, `path`, and `name`. 127 | fn get_mut(&mut self, domain: &str, path: &str, name: &str) -> Option<&mut Cookie<'static>> { 128 | self.get_mut_any(domain, path, name).and_then(|cookie| { 129 | if cookie.is_expired() { 130 | None 131 | } else { 132 | Some(cookie) 133 | } 134 | }) 135 | } 136 | 137 | /// Returns a reference to the (possibly __expired__) `Cookie` corresponding to the specified 138 | /// `domain`, `path`, and `name`. 139 | pub fn get_any(&self, domain: &str, path: &str, name: &str) -> Option<&Cookie<'static>> { 140 | self.cookies.get(domain).and_then(|domain_cookies| { 141 | domain_cookies 142 | .get(path) 143 | .and_then(|path_cookies| path_cookies.get(name)) 144 | }) 145 | } 146 | 147 | /// Returns a mutable reference to the (possibly __expired__) `Cookie` corresponding to the 148 | /// specified `domain`, `path`, and `name`. 149 | fn get_mut_any( 150 | &mut self, 151 | domain: &str, 152 | path: &str, 153 | name: &str, 154 | ) -> Option<&mut Cookie<'static>> { 155 | self.cookies.get_mut(domain).and_then(|domain_cookies| { 156 | domain_cookies 157 | .get_mut(path) 158 | .and_then(|path_cookies| path_cookies.get_mut(name)) 159 | }) 160 | } 161 | 162 | /// Removes a `Cookie` from the store, returning the `Cookie` if it was in the store 163 | pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option> { 164 | #[cfg(not(feature = "preserve_order"))] 165 | fn map_remove(map: &mut Map, key: &Q) -> Option 166 | where 167 | K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, 168 | Q: std::cmp::Eq + std::hash::Hash + ?Sized, 169 | { 170 | map.remove(key) 171 | } 172 | #[cfg(feature = "preserve_order")] 173 | fn map_remove(map: &mut Map, key: &Q) -> Option 174 | where 175 | K: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash, 176 | Q: std::cmp::Eq + std::hash::Hash + ?Sized, 177 | { 178 | map.shift_remove(key) 179 | } 180 | 181 | let (removed, remove_domain) = match self.cookies.get_mut(domain) { 182 | None => (None, false), 183 | Some(domain_cookies) => { 184 | let (removed, remove_path) = match domain_cookies.get_mut(path) { 185 | None => (None, false), 186 | Some(path_cookies) => { 187 | let removed = map_remove(path_cookies, name); 188 | (removed, path_cookies.is_empty()) 189 | } 190 | }; 191 | 192 | if remove_path { 193 | map_remove(domain_cookies, path); 194 | (removed, domain_cookies.is_empty()) 195 | } else { 196 | (removed, false) 197 | } 198 | } 199 | }; 200 | 201 | if remove_domain { 202 | map_remove(&mut self.cookies, domain); 203 | } 204 | 205 | removed 206 | } 207 | 208 | /// Returns a collection of references to __unexpired__ cookies that path- and domain-match 209 | /// `request_url`, as well as having HttpOnly and Secure attributes compatible with the 210 | /// `request_url`. 211 | pub fn matches(&self, request_url: &Url) -> Vec<&Cookie<'static>> { 212 | // although we domain_match and path_match as we descend through the tree, we 213 | // still need to 214 | // do a full Cookie::matches() check in the last filter. Otherwise, we cannot 215 | // properly deal 216 | // with HostOnly Cookies. 217 | let cookies = self 218 | .cookies 219 | .iter() 220 | .filter(|&(d, _)| domain_match(d, request_url)) 221 | .flat_map(|(_, dcs)| { 222 | dcs.iter() 223 | .filter(|&(p, _)| path_match(p, request_url)) 224 | .flat_map(|(_, pcs)| { 225 | pcs.values() 226 | .filter(|c| !c.is_expired() && c.matches(request_url)) 227 | }) 228 | }); 229 | match (!is_http_scheme(request_url), !is_secure(request_url)) { 230 | (true, true) => cookies 231 | .filter(|c| !c.http_only().unwrap_or(false) && !c.secure().unwrap_or(false)) 232 | .collect(), 233 | (true, false) => cookies 234 | .filter(|c| !c.http_only().unwrap_or(false)) 235 | .collect(), 236 | (false, true) => cookies.filter(|c| !c.secure().unwrap_or(false)).collect(), 237 | (false, false) => cookies.collect(), 238 | } 239 | } 240 | 241 | /// Parses a new `Cookie` from `cookie_str` and inserts it into the store. 242 | pub fn parse(&mut self, cookie_str: &str, request_url: &Url) -> InsertResult { 243 | Cookie::parse(cookie_str, request_url) 244 | .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) 245 | } 246 | 247 | /// Converts a `cookie::Cookie` (from the `cookie` crate) into a `cookie_store::Cookie` and 248 | /// inserts it into the store. 249 | pub fn insert_raw(&mut self, cookie: &RawCookie<'_>, request_url: &Url) -> InsertResult { 250 | Cookie::try_from_raw_cookie(cookie, request_url) 251 | .and_then(|cookie| self.insert(cookie.into_owned(), request_url)) 252 | } 253 | 254 | /// Inserts `cookie`, received from `request_url`, into the store, following the rules of the 255 | /// [IETF RFC6265 Storage Model](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3). If the 256 | /// `Cookie` is __unexpired__ and is successfully inserted, returns 257 | /// `Ok(StoreAction::Inserted)`. If the `Cookie` is __expired__ *and* matches an existing 258 | /// `Cookie` in the store, the existing `Cookie` wil be `expired()` and 259 | /// `Ok(StoreAction::ExpiredExisting)` will be returned. 260 | pub fn insert(&mut self, cookie: Cookie<'static>, request_url: &Url) -> InsertResult { 261 | if cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { 262 | // If the cookie was received from a "non-HTTP" API and the 263 | // cookie's http-only-flag is set, abort these steps and ignore the 264 | // cookie entirely. 265 | return Err(CookieError::NonHttpScheme); 266 | } 267 | #[cfg(feature = "public_suffix")] 268 | let mut cookie = cookie; 269 | #[cfg(feature = "public_suffix")] 270 | if let Some(ref psl) = self.public_suffix_list { 271 | // If the user agent is configured to reject "public suffixes" 272 | if cookie.domain.is_public_suffix(psl) { 273 | // and the domain-attribute is a public suffix: 274 | if cookie.domain.host_is_identical(request_url) { 275 | // If the domain-attribute is identical to the canonicalized 276 | // request-host: 277 | // Let the domain-attribute be the empty string. 278 | // (NB: at this point, an empty domain-attribute should be represented 279 | // as the HostOnly variant of CookieDomain) 280 | cookie.domain = crate::cookie_domain::CookieDomain::host_only(request_url)?; 281 | } else { 282 | // Otherwise: 283 | // Ignore the cookie entirely and abort these steps. 284 | return Err(CookieError::PublicSuffix); 285 | } 286 | } 287 | } 288 | if !cookie.domain.matches(request_url) { 289 | // If the canonicalized request-host does not domain-match the 290 | // domain-attribute: 291 | // Ignore the cookie entirely and abort these steps. 292 | return Err(CookieError::DomainMismatch); 293 | } 294 | // NB: we do not bail out above on is_expired(), as servers can remove a cookie 295 | // by sending 296 | // an expired one, so we need to do the old_cookie check below before checking 297 | // is_expired() on an incoming cookie 298 | 299 | { 300 | // At this point in parsing, any non-present Domain attribute should have been 301 | // converted into a HostOnly variant 302 | let cookie_domain = cookie 303 | .domain 304 | .as_cow() 305 | .ok_or_else(|| CookieError::UnspecifiedDomain)?; 306 | if let Some(old_cookie) = self.get_mut(&cookie_domain, &cookie.path, cookie.name()) { 307 | if old_cookie.http_only().unwrap_or(false) && !is_http_scheme(request_url) { 308 | // 2. If the newly created cookie was received from a "non-HTTP" 309 | // API and the old-cookie's http-only-flag is set, abort these 310 | // steps and ignore the newly created cookie entirely. 311 | return Err(CookieError::NonHttpScheme); 312 | } else if cookie.is_expired() { 313 | old_cookie.expire(); 314 | return Ok(StoreAction::ExpiredExisting); 315 | } 316 | } 317 | } 318 | 319 | if !cookie.is_expired() { 320 | Ok( 321 | if self 322 | .cookies 323 | .entry(String::from(&cookie.domain)) 324 | .or_insert_with(Map::new) 325 | .entry(String::from(&cookie.path)) 326 | .or_insert_with(Map::new) 327 | .insert(cookie.name().to_owned(), cookie) 328 | .is_none() 329 | { 330 | StoreAction::Inserted 331 | } else { 332 | StoreAction::UpdatedExisting 333 | }, 334 | ) 335 | } else { 336 | Err(CookieError::Expired) 337 | } 338 | } 339 | 340 | /// Clear the contents of the store 341 | pub fn clear(&mut self) { 342 | self.cookies.clear() 343 | } 344 | 345 | /// An iterator visiting all the __unexpired__ cookies in the store 346 | pub fn iter_unexpired<'a>(&'a self) -> impl Iterator> + 'a { 347 | self.cookies 348 | .values() 349 | .flat_map(|dcs| dcs.values()) 350 | .flat_map(|pcs| pcs.values()) 351 | .filter(|c| !c.is_expired()) 352 | } 353 | 354 | /// An iterator visiting all (including __expired__) cookies in the store 355 | pub fn iter_any<'a>(&'a self) -> impl Iterator> + 'a { 356 | self.cookies 357 | .values() 358 | .flat_map(|dcs| dcs.values()) 359 | .flat_map(|pcs| pcs.values()) 360 | } 361 | 362 | /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string` 363 | /// and write them to `writer` 364 | pub fn save(&self, writer: &mut W, cookie_to_string: F) -> StoreResult<()> 365 | where 366 | W: Write, 367 | F: Fn(&Cookie<'static>) -> Result, 368 | crate::Error: From, 369 | { 370 | for cookie in self.iter_unexpired().filter_map(|c| { 371 | if c.is_persistent() { 372 | Some(cookie_to_string(c)) 373 | } else { 374 | None 375 | } 376 | }) { 377 | writeln!(writer, "{}", cookie?)?; 378 | } 379 | Ok(()) 380 | } 381 | 382 | /// Serialize all (including __expired__ and __non-persistent__) cookies in the store with `cookie_to_string` and write them to `writer` 383 | pub fn save_incl_expired_and_nonpersistent( 384 | &self, 385 | writer: &mut W, 386 | cookie_to_string: F, 387 | ) -> StoreResult<()> 388 | where 389 | W: Write, 390 | F: Fn(&Cookie<'static>) -> Result, 391 | crate::Error: From, 392 | { 393 | for cookie in self.iter_any() { 394 | writeln!(writer, "{}", cookie_to_string(cookie)?)?; 395 | } 396 | Ok(()) 397 | } 398 | 399 | /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__ 400 | /// cookies 401 | pub fn load(reader: R, cookie_from_str: F) -> StoreResult 402 | where 403 | R: BufRead, 404 | F: Fn(&str) -> Result, E>, 405 | crate::Error: From, 406 | { 407 | CookieStore::load_from(reader, cookie_from_str, false) 408 | } 409 | 410 | /// Load cookies from `reader`, deserializing with `cookie_from_str`, loading both __unexpired__ 411 | /// and __expired__ cookies 412 | pub fn load_all(reader: R, cookie_from_str: F) -> StoreResult 413 | where 414 | R: BufRead, 415 | F: Fn(&str) -> Result, E>, 416 | crate::Error: From, 417 | { 418 | CookieStore::load_from(reader, cookie_from_str, true) 419 | } 420 | 421 | fn load_from( 422 | reader: R, 423 | cookie_from_str: F, 424 | include_expired: bool, 425 | ) -> StoreResult 426 | where 427 | R: BufRead, 428 | F: Fn(&str) -> Result, E>, 429 | crate::Error: From, 430 | { 431 | let cookies = reader.lines().map(|line_result| { 432 | line_result 433 | .map_err(Into::into) 434 | .and_then(|line| cookie_from_str(&line).map_err(crate::Error::from)) 435 | }); 436 | Self::from_cookies(cookies, include_expired) 437 | } 438 | 439 | /// Create a `CookieStore` from an iterator of `Cookie` values. When 440 | /// `include_expired` is `true`, both __expired__ and __unexpired__ cookies in the incoming 441 | /// iterator will be included in the produced `CookieStore`; otherwise, only 442 | /// __unexpired__ cookies will be included, and __expired__ cookies filtered 443 | /// out. 444 | pub fn from_cookies(iter: I, include_expired: bool) -> Result 445 | where 446 | I: IntoIterator, E>>, 447 | { 448 | let mut cookies = Map::new(); 449 | for cookie in iter { 450 | let cookie = cookie?; 451 | if include_expired || !cookie.is_expired() { 452 | cookies 453 | .entry(String::from(&cookie.domain)) 454 | .or_insert_with(Map::new) 455 | .entry(String::from(&cookie.path)) 456 | .or_insert_with(Map::new) 457 | .insert(cookie.name().to_owned(), cookie); 458 | } 459 | } 460 | Ok(Self { 461 | cookies, 462 | #[cfg(feature = "public_suffix")] 463 | public_suffix_list: None, 464 | }) 465 | } 466 | 467 | pub fn new( 468 | #[cfg(feature = "public_suffix")] public_suffix_list: Option, 469 | ) -> Self { 470 | Self { 471 | cookies: DomainMap::new(), 472 | #[cfg(feature = "public_suffix")] 473 | public_suffix_list, 474 | } 475 | } 476 | } 477 | 478 | 479 | #[cfg(feature = "serde_json")] 480 | /// Legacy serialization implementations. These methods do **not** produce/consume valid JSON output compatible with 481 | /// typical JSON libraries/tools. 482 | impl CookieStore { 483 | /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and 484 | /// write them to `writer` 485 | /// 486 | /// __NB__: this method does not produce valid JSON which can be directly loaded; such output 487 | /// must be loaded via the corresponding method [CookieStore::load_json]. For a more 488 | /// robust/universal 489 | /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this 490 | /// method. 491 | #[deprecated( 492 | since = "0.22.0", 493 | note = "See `cookie_store::serde` modules for more robust de/serialization options" 494 | )] 495 | pub fn save_json(&self, writer: &mut W) -> StoreResult<()> { 496 | self.save(writer, ::serde_json::to_string) 497 | } 498 | 499 | /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to JSON format and write them to `writer` 500 | /// 501 | /// __NB__: this method does not produce valid JSON which can be directly loaded; such output 502 | /// must be loaded via the corresponding method [CookieStore::load_json]. For a more 503 | /// robust/universal 504 | /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this 505 | /// method. 506 | #[deprecated( 507 | since = "0.22.0", 508 | note = "See `cookie_store::serde` modules for more robust de/serialization options" 509 | )] 510 | pub fn save_incl_expired_and_nonpersistent_json( 511 | &self, 512 | writer: &mut W, 513 | ) -> StoreResult<()> { 514 | self.save_incl_expired_and_nonpersistent(writer, ::serde_json::to_string) 515 | } 516 | 517 | /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies 518 | /// 519 | /// __NB__: this method does not expect true valid JSON; it is designed to load output 520 | /// from the corresponding method [CookieStore::save_json]. For a more robust/universal 521 | /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this 522 | /// method. 523 | #[deprecated( 524 | since = "0.22.0", 525 | note = "See `cookie_store::serde` modules for more robust de/serialization options" 526 | )] 527 | pub fn load_json(reader: R) -> StoreResult { 528 | CookieStore::load(reader, |cookie| ::serde_json::from_str(cookie)) 529 | } 530 | 531 | /// Load JSON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies 532 | /// 533 | /// __NB__: this method does not expect true valid JSON; it is designed to load output 534 | /// from the corresponding method [CookieStore::save_json]. For a more robust/universal 535 | /// JSON format, see [crate::serde::json], which produces output __incompatible__ with this 536 | /// method. 537 | #[deprecated( 538 | since = "0.22.0", 539 | note = "See `cookie_store::serde` modules for more robust de/serialization options" 540 | )] 541 | pub fn load_json_all(reader: R) -> StoreResult { 542 | CookieStore::load_all(reader, |cookie| ::serde_json::from_str(cookie)) 543 | } 544 | } 545 | 546 | #[cfg(feature = "serde")] 547 | /// Legacy de/serialization implementation which elides the collection-nature of the contained 548 | /// cookies. Suitable for line-oriented cookie persistence, but prefer/consider 549 | /// `cookie_store::serde` modules for more universally consumable serialization formats. 550 | mod serde_legacy { 551 | use serde::de::{SeqAccess, Visitor}; 552 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 553 | 554 | impl Serialize for super::CookieStore { 555 | fn serialize(&self, serializer: S) -> Result 556 | where 557 | S: Serializer, 558 | { 559 | serializer.collect_seq(self.iter_unexpired().filter(|c| c.is_persistent())) 560 | } 561 | } 562 | 563 | struct CookieStoreVisitor; 564 | 565 | impl<'de> Visitor<'de> for CookieStoreVisitor { 566 | type Value = super::CookieStore; 567 | 568 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 569 | write!(formatter, "a sequence of cookies") 570 | } 571 | 572 | fn visit_seq(self, mut seq: A) -> Result 573 | where 574 | A: SeqAccess<'de>, 575 | { 576 | super::CookieStore::from_cookies(std::iter::from_fn(|| seq.next_element().transpose()), false) 577 | } 578 | } 579 | 580 | impl<'de> Deserialize<'de> for super::CookieStore { 581 | fn deserialize(deserializer: D) -> Result 582 | where 583 | D: Deserializer<'de>, 584 | { 585 | deserializer.deserialize_seq(CookieStoreVisitor) 586 | } 587 | } 588 | } 589 | 590 | #[cfg(test)] 591 | mod tests { 592 | use super::CookieStore; 593 | use super::{InsertResult, StoreAction}; 594 | use crate::cookie::Cookie; 595 | use crate::CookieError; 596 | use ::cookie::Cookie as RawCookie; 597 | use time::OffsetDateTime; 598 | 599 | use crate::utils::test as test_utils; 600 | 601 | macro_rules! inserted { 602 | ($e: expr) => { 603 | assert_eq!(Ok(StoreAction::Inserted), $e) 604 | }; 605 | } 606 | macro_rules! updated { 607 | ($e: expr) => { 608 | assert_eq!(Ok(StoreAction::UpdatedExisting), $e) 609 | }; 610 | } 611 | macro_rules! expired_existing { 612 | ($e: expr) => { 613 | assert_eq!(Ok(StoreAction::ExpiredExisting), $e) 614 | }; 615 | } 616 | macro_rules! domain_mismatch { 617 | ($e: expr) => { 618 | assert_eq!(Err(CookieError::DomainMismatch), $e) 619 | }; 620 | } 621 | macro_rules! non_http_scheme { 622 | ($e: expr) => { 623 | assert_eq!(Err(CookieError::NonHttpScheme), $e) 624 | }; 625 | } 626 | macro_rules! non_rel_scheme { 627 | ($e: expr) => { 628 | assert_eq!(Err(CookieError::NonRelativeScheme), $e) 629 | }; 630 | } 631 | macro_rules! expired_err { 632 | ($e: expr) => { 633 | assert_eq!(Err(CookieError::Expired), $e) 634 | }; 635 | } 636 | macro_rules! values_are { 637 | ($store: expr, $url: expr, $values: expr) => {{ 638 | let mut matched_values = $store 639 | .matches(&test_utils::url($url)) 640 | .iter() 641 | .map(|c| &c.value()[..]) 642 | .collect::>(); 643 | matched_values.sort(); 644 | 645 | let mut values: Vec<&str> = $values; 646 | values.sort(); 647 | 648 | assert!( 649 | matched_values == values, 650 | "\n{:?}\n!=\n{:?}\n", 651 | matched_values, 652 | values 653 | ); 654 | }}; 655 | } 656 | 657 | fn add_cookie( 658 | store: &mut CookieStore, 659 | cookie: &str, 660 | url: &str, 661 | expires: Option, 662 | max_age: Option, 663 | ) -> InsertResult { 664 | store.insert( 665 | test_utils::make_cookie(cookie, url, expires, max_age), 666 | &test_utils::url(url), 667 | ) 668 | } 669 | 670 | fn make_match_store() -> CookieStore { 671 | let mut store = CookieStore::default(); 672 | inserted!(add_cookie( 673 | &mut store, 674 | "cookie1=1", 675 | "http://example.com/foo/bar", 676 | None, 677 | Some(60 * 5), 678 | )); 679 | inserted!(add_cookie( 680 | &mut store, 681 | "cookie2=2; Secure", 682 | "https://example.com/sec/", 683 | None, 684 | Some(60 * 5), 685 | )); 686 | inserted!(add_cookie( 687 | &mut store, 688 | "cookie3=3; HttpOnly", 689 | "https://example.com/sec/", 690 | None, 691 | Some(60 * 5), 692 | )); 693 | inserted!(add_cookie( 694 | &mut store, 695 | "cookie4=4; Secure; HttpOnly", 696 | "https://example.com/sec/", 697 | None, 698 | Some(60 * 5), 699 | )); 700 | inserted!(add_cookie( 701 | &mut store, 702 | "cookie5=5", 703 | "http://example.com/foo/", 704 | None, 705 | Some(60 * 5), 706 | )); 707 | inserted!(add_cookie( 708 | &mut store, 709 | "cookie6=6", 710 | "http://example.com/", 711 | None, 712 | Some(60 * 5), 713 | )); 714 | inserted!(add_cookie( 715 | &mut store, 716 | "cookie7=7", 717 | "http://bar.example.com/foo/", 718 | None, 719 | Some(60 * 5), 720 | )); 721 | 722 | inserted!(add_cookie( 723 | &mut store, 724 | "cookie8=8", 725 | "http://example.org/foo/bar", 726 | None, 727 | Some(60 * 5), 728 | )); 729 | inserted!(add_cookie( 730 | &mut store, 731 | "cookie9=9", 732 | "http://bar.example.org/foo/bar", 733 | None, 734 | Some(60 * 5), 735 | )); 736 | store 737 | } 738 | 739 | macro_rules! check_matches { 740 | ($store: expr) => {{ 741 | values_are!($store, "http://unknowndomain.org/foo/bar", vec![]); 742 | values_are!($store, "http://example.org/foo/bar", vec!["8"]); 743 | values_are!($store, "http://example.org/bus/bar", vec![]); 744 | values_are!($store, "http://bar.example.org/foo/bar", vec!["9"]); 745 | values_are!($store, "http://bar.example.org/bus/bar", vec![]); 746 | values_are!( 747 | $store, 748 | "https://example.com/sec/foo", 749 | vec!["6", "4", "3", "2"] 750 | ); 751 | values_are!($store, "http://example.com/sec/foo", vec!["6", "3"]); 752 | values_are!($store, "ftp://example.com/sec/foo", vec!["6"]); 753 | values_are!($store, "http://bar.example.com/foo/bar/bus", vec!["7"]); 754 | values_are!( 755 | $store, 756 | "http://example.com/foo/bar/bus", 757 | vec!["1", "5", "6"] 758 | ); 759 | }}; 760 | } 761 | 762 | #[test] 763 | fn insert_raw() { 764 | let mut store = CookieStore::default(); 765 | inserted!(store.insert_raw( 766 | &RawCookie::parse("cookie1=value1").unwrap(), 767 | &test_utils::url("http://example.com/foo/bar"), 768 | )); 769 | non_rel_scheme!(store.insert_raw( 770 | &RawCookie::parse("cookie1=value1").unwrap(), 771 | &test_utils::url("data:nonrelativescheme"), 772 | )); 773 | non_http_scheme!(store.insert_raw( 774 | &RawCookie::parse("cookie1=value1; HttpOnly").unwrap(), 775 | &test_utils::url("ftp://example.com/"), 776 | )); 777 | expired_existing!(store.insert_raw( 778 | &RawCookie::parse("cookie1=value1; Max-Age=0").unwrap(), 779 | &test_utils::url("http://example.com/foo/bar"), 780 | )); 781 | expired_err!(store.insert_raw( 782 | &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), 783 | &test_utils::url("http://example.com/foo/bar"), 784 | )); 785 | updated!(store.insert_raw( 786 | &RawCookie::parse("cookie1=value1").unwrap(), 787 | &test_utils::url("http://example.com/foo/bar"), 788 | )); 789 | expired_existing!(store.insert_raw( 790 | &RawCookie::parse("cookie1=value1; Max-Age=-1").unwrap(), 791 | &test_utils::url("http://example.com/foo/bar"), 792 | )); 793 | domain_mismatch!(store.insert_raw( 794 | &RawCookie::parse("cookie1=value1; Domain=bar.example.com").unwrap(), 795 | &test_utils::url("http://example.com/foo/bar"), 796 | )); 797 | } 798 | 799 | #[test] 800 | fn parse() { 801 | let mut store = CookieStore::default(); 802 | inserted!(store.parse( 803 | "cookie1=value1", 804 | &test_utils::url("http://example.com/foo/bar"), 805 | )); 806 | non_rel_scheme!(store.parse("cookie1=value1", &test_utils::url("data:nonrelativescheme"),)); 807 | non_http_scheme!(store.parse( 808 | "cookie1=value1; HttpOnly", 809 | &test_utils::url("ftp://example.com/"), 810 | )); 811 | expired_existing!(store.parse( 812 | "cookie1=value1; Max-Age=0", 813 | &test_utils::url("http://example.com/foo/bar"), 814 | )); 815 | expired_err!(store.parse( 816 | "cookie1=value1; Max-Age=-1", 817 | &test_utils::url("http://example.com/foo/bar"), 818 | )); 819 | updated!(store.parse( 820 | "cookie1=value1", 821 | &test_utils::url("http://example.com/foo/bar"), 822 | )); 823 | expired_existing!(store.parse( 824 | "cookie1=value1; Max-Age=-1", 825 | &test_utils::url("http://example.com/foo/bar"), 826 | )); 827 | domain_mismatch!(store.parse( 828 | "cookie1=value1; Domain=bar.example.com", 829 | &test_utils::url("http://example.com/foo/bar"), 830 | )); 831 | } 832 | 833 | #[test] 834 | fn domains() { 835 | let mut store = CookieStore::default(); 836 | // The user agent will reject cookies unless the Domain attribute 837 | // specifies a scope for the cookie that would include the origin 838 | // server. For example, the user agent will accept a cookie with a 839 | // Domain attribute of "example.com" or of "foo.example.com" from 840 | // foo.example.com, but the user agent will not accept a cookie with a 841 | // Domain attribute of "bar.example.com" or of "baz.foo.example.com". 842 | fn domain_cookie_from(domain: &str, request_url: &str) -> Cookie<'static> { 843 | let cookie_str = format!("cookie1=value1; Domain={}", domain); 844 | Cookie::parse(cookie_str, &test_utils::url(request_url)).unwrap() 845 | } 846 | 847 | { 848 | let request_url = test_utils::url("http://foo.example.com"); 849 | // foo.example.com can submit cookies for example.com and foo.example.com 850 | inserted!(store.insert( 851 | domain_cookie_from("example.com", "http://foo.example.com",), 852 | &request_url, 853 | )); 854 | updated!(store.insert( 855 | domain_cookie_from(".example.com", "http://foo.example.com",), 856 | &request_url, 857 | )); 858 | inserted!(store.insert( 859 | domain_cookie_from("foo.example.com", "http://foo.example.com",), 860 | &request_url, 861 | )); 862 | updated!(store.insert( 863 | domain_cookie_from(".foo.example.com", "http://foo.example.com",), 864 | &request_url, 865 | )); 866 | // not for bar.example.com 867 | domain_mismatch!(store.insert( 868 | domain_cookie_from("bar.example.com", "http://bar.example.com",), 869 | &request_url, 870 | )); 871 | domain_mismatch!(store.insert( 872 | domain_cookie_from(".bar.example.com", "http://bar.example.com",), 873 | &request_url, 874 | )); 875 | // not for bar.foo.example.com 876 | domain_mismatch!(store.insert( 877 | domain_cookie_from("bar.foo.example.com", "http://bar.foo.example.com",), 878 | &request_url, 879 | )); 880 | domain_mismatch!(store.insert( 881 | domain_cookie_from(".bar.foo.example.com", "http://bar.foo.example.com",), 882 | &request_url, 883 | )); 884 | } 885 | 886 | { 887 | let request_url = test_utils::url("http://bar.example.com"); 888 | // bar.example.com can submit for example.com and bar.example.com 889 | updated!(store.insert( 890 | domain_cookie_from("example.com", "http://foo.example.com",), 891 | &request_url, 892 | )); 893 | updated!(store.insert( 894 | domain_cookie_from(".example.com", "http://foo.example.com",), 895 | &request_url, 896 | )); 897 | inserted!(store.insert( 898 | domain_cookie_from("bar.example.com", "http://bar.example.com",), 899 | &request_url, 900 | )); 901 | updated!(store.insert( 902 | domain_cookie_from(".bar.example.com", "http://bar.example.com",), 903 | &request_url, 904 | )); 905 | // bar.example.com cannot submit for foo.example.com 906 | domain_mismatch!(store.insert( 907 | domain_cookie_from("foo.example.com", "http://foo.example.com",), 908 | &request_url, 909 | )); 910 | domain_mismatch!(store.insert( 911 | domain_cookie_from(".foo.example.com", "http://foo.example.com",), 912 | &request_url, 913 | )); 914 | } 915 | { 916 | let request_url = test_utils::url("http://example.com"); 917 | // example.com can submit for example.com 918 | updated!(store.insert( 919 | domain_cookie_from("example.com", "http://foo.example.com",), 920 | &request_url, 921 | )); 922 | updated!(store.insert( 923 | domain_cookie_from(".example.com", "http://foo.example.com",), 924 | &request_url, 925 | )); 926 | // example.com cannot submit for foo.example.com or bar.example.com 927 | domain_mismatch!(store.insert( 928 | domain_cookie_from("foo.example.com", "http://foo.example.com",), 929 | &request_url, 930 | )); 931 | domain_mismatch!(store.insert( 932 | domain_cookie_from(".foo.example.com", "http://foo.example.com",), 933 | &request_url, 934 | )); 935 | domain_mismatch!(store.insert( 936 | domain_cookie_from("bar.example.com", "http://bar.example.com",), 937 | &request_url, 938 | )); 939 | domain_mismatch!(store.insert( 940 | domain_cookie_from(".bar.example.com", "http://bar.example.com",), 941 | &request_url, 942 | )); 943 | } 944 | } 945 | 946 | #[test] 947 | fn http_only() { 948 | let mut store = CookieStore::default(); 949 | let c = Cookie::parse( 950 | "cookie1=value1; HttpOnly", 951 | &test_utils::url("http://example.com/foo/bar"), 952 | ) 953 | .unwrap(); 954 | // cannot add a HttpOnly cookies from a non-http source 955 | non_http_scheme!(store.insert(c, &test_utils::url("ftp://example.com/foo/bar"),)); 956 | } 957 | 958 | #[test] 959 | fn clear() { 960 | let mut store = CookieStore::default(); 961 | inserted!(add_cookie( 962 | &mut store, 963 | "cookie1=value1", 964 | "http://example.com/foo/bar", 965 | Some(test_utils::in_days(1)), 966 | None, 967 | )); 968 | assert!(store.iter_any().any(|c| c.name_value() == ("cookie1", "value1")), "did not find expected cookie1=value1 cookie in store"); 969 | store.clear(); 970 | assert!(store.iter_any().count() == 0, "found unexpected cookies in cleared store"); 971 | } 972 | 973 | #[test] 974 | fn add_and_get() { 975 | let mut store = CookieStore::default(); 976 | assert!(store.get("example.com", "/foo", "cookie1").is_none()); 977 | 978 | inserted!(add_cookie( 979 | &mut store, 980 | "cookie1=value1", 981 | "http://example.com/foo/bar", 982 | None, 983 | None, 984 | )); 985 | assert!(store.get("example.com", "/foo/bar", "cookie1").is_none()); 986 | assert!(store.get("example.com", "/foo", "cookie2").is_none()); 987 | assert!(store.get("example.org", "/foo", "cookie1").is_none()); 988 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); 989 | 990 | updated!(add_cookie( 991 | &mut store, 992 | "cookie1=value2", 993 | "http://example.com/foo/bar", 994 | None, 995 | None, 996 | )); 997 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); 998 | 999 | inserted!(add_cookie( 1000 | &mut store, 1001 | "cookie2=value3", 1002 | "http://example.com/foo/bar", 1003 | None, 1004 | None, 1005 | )); 1006 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); 1007 | assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); 1008 | 1009 | inserted!(add_cookie( 1010 | &mut store, 1011 | "cookie3=value4; HttpOnly", 1012 | "http://example.com/foo/bar", 1013 | None, 1014 | None, 1015 | )); 1016 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); 1017 | assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); 1018 | assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); 1019 | 1020 | non_http_scheme!(add_cookie( 1021 | &mut store, 1022 | "cookie3=value5", 1023 | "ftp://example.com/foo/bar", 1024 | None, 1025 | None, 1026 | )); 1027 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value2"); 1028 | assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value3"); 1029 | assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value4"); 1030 | } 1031 | 1032 | #[test] 1033 | fn matches() { 1034 | let store = make_match_store(); 1035 | check_matches!(&store); 1036 | } 1037 | 1038 | fn matches_are(store: &CookieStore, url: &str, exp: Vec<&str>) { 1039 | let matches = store 1040 | .matches(&test_utils::url(url)) 1041 | .iter() 1042 | .map(|c| format!("{}={}", c.name(), c.value())) 1043 | .collect::>(); 1044 | for e in &exp { 1045 | assert!( 1046 | matches.iter().any(|m| &m[..] == *e), 1047 | "{}: matches missing '{}'\nmatches: {:?}\n exp: {:?}", 1048 | url, 1049 | e, 1050 | matches, 1051 | exp 1052 | ); 1053 | } 1054 | assert!( 1055 | matches.len() == exp.len(), 1056 | "{}: matches={:?} != exp={:?}", 1057 | url, 1058 | matches, 1059 | exp 1060 | ); 1061 | } 1062 | 1063 | #[test] 1064 | fn some_non_https_uris_are_secure() { 1065 | // Matching the list in Firefox's regression test: 1066 | // https://hg.mozilla.org/integration/autoland/rev/c4d13b3ca1e2 1067 | let secure_uris = vec![ 1068 | "http://localhost", 1069 | "http://localhost:1234", 1070 | "http://127.0.0.1", 1071 | "http://127.0.0.2", 1072 | "http://127.1.0.1", 1073 | "http://[::1]", 1074 | ]; 1075 | for secure_uri in secure_uris { 1076 | let mut store = CookieStore::default(); 1077 | inserted!(add_cookie( 1078 | &mut store, 1079 | "cookie1=1a; Secure", 1080 | secure_uri, 1081 | None, 1082 | None, 1083 | )); 1084 | matches_are(&store, secure_uri, vec!["cookie1=1a"]); 1085 | } 1086 | } 1087 | 1088 | #[cfg(feature = "serde_json")] 1089 | macro_rules! dump_json { 1090 | ($e: expr, $i: ident) => {{ 1091 | use serde_json; 1092 | println!(""); 1093 | println!( 1094 | "==== {}: {} ====", 1095 | $e, 1096 | time::OffsetDateTime::now_utc() 1097 | .format(crate::rfc3339_fmt::RFC3339_FORMAT) 1098 | .unwrap() 1099 | ); 1100 | for c in $i.iter_any() { 1101 | println!( 1102 | "{} {}", 1103 | if c.is_expired() { 1104 | "XXXXX" 1105 | } else if c.is_persistent() { 1106 | "PPPPP" 1107 | } else { 1108 | " " 1109 | }, 1110 | serde_json::to_string(c).unwrap() 1111 | ); 1112 | println!("----------------"); 1113 | } 1114 | println!("================"); 1115 | }}; 1116 | } 1117 | 1118 | #[test] 1119 | fn domain_collisions() { 1120 | let mut store = CookieStore::default(); 1121 | // - HostOnly, so no collisions here 1122 | inserted!(add_cookie( 1123 | &mut store, 1124 | "cookie1=1a", 1125 | "http://foo.bus.example.com/", 1126 | None, 1127 | None, 1128 | )); 1129 | inserted!(add_cookie( 1130 | &mut store, 1131 | "cookie1=1b", 1132 | "http://bus.example.com/", 1133 | None, 1134 | None, 1135 | )); 1136 | // - Suffix 1137 | // both cookie2's domain-match bus.example.com 1138 | inserted!(add_cookie( 1139 | &mut store, 1140 | "cookie2=2a; Domain=bus.example.com", 1141 | "http://foo.bus.example.com/", 1142 | None, 1143 | None, 1144 | )); 1145 | inserted!(add_cookie( 1146 | &mut store, 1147 | "cookie2=2b; Domain=example.com", 1148 | "http://bus.example.com/", 1149 | None, 1150 | None, 1151 | )); 1152 | #[cfg(feature = "serde_json")] 1153 | dump_json!("domain_collisions", store); 1154 | matches_are( 1155 | &store, 1156 | "http://foo.bus.example.com/", 1157 | vec!["cookie1=1a", "cookie2=2a", "cookie2=2b"], 1158 | ); 1159 | matches_are( 1160 | &store, 1161 | "http://bus.example.com/", 1162 | vec!["cookie1=1b", "cookie2=2a", "cookie2=2b"], 1163 | ); 1164 | matches_are(&store, "http://example.com/", vec!["cookie2=2b"]); 1165 | matches_are(&store, "http://foo.example.com/", vec!["cookie2=2b"]); 1166 | } 1167 | 1168 | #[test] 1169 | fn path_collisions() { 1170 | let mut store = CookieStore::default(); 1171 | // will be default-path: /foo/bar, and /foo, resp. 1172 | // both should match /foo/bar/ 1173 | inserted!(add_cookie( 1174 | &mut store, 1175 | "cookie3=3a", 1176 | "http://bus.example.com/foo/bar/", 1177 | None, 1178 | None, 1179 | )); 1180 | inserted!(add_cookie( 1181 | &mut store, 1182 | "cookie3=3b", 1183 | "http://bus.example.com/foo/", 1184 | None, 1185 | None, 1186 | )); 1187 | // - Path set explicitly 1188 | inserted!(add_cookie( 1189 | &mut store, 1190 | "cookie4=4a; Path=/foo/bar/", 1191 | "http://bus.example.com/", 1192 | None, 1193 | None, 1194 | )); 1195 | inserted!(add_cookie( 1196 | &mut store, 1197 | "cookie4=4b; Path=/foo/", 1198 | "http://bus.example.com/", 1199 | None, 1200 | None, 1201 | )); 1202 | #[cfg(feature = "serde_json")] 1203 | dump_json!("path_collisions", store); 1204 | matches_are( 1205 | &store, 1206 | "http://bus.example.com/foo/bar/", 1207 | vec!["cookie3=3a", "cookie3=3b", "cookie4=4a", "cookie4=4b"], 1208 | ); 1209 | // Agrees w/ chrome, but not FF... FF also sends cookie4=4a, but this should be 1210 | // a path-match 1211 | // fail since request-uri /foo/bar is a *prefix* of the cookie path /foo/bar/ 1212 | matches_are( 1213 | &store, 1214 | "http://bus.example.com/foo/bar", 1215 | vec!["cookie3=3a", "cookie3=3b", "cookie4=4b"], 1216 | ); 1217 | matches_are( 1218 | &store, 1219 | "http://bus.example.com/foo/ba", 1220 | vec!["cookie3=3b", "cookie4=4b"], 1221 | ); 1222 | matches_are( 1223 | &store, 1224 | "http://bus.example.com/foo/", 1225 | vec!["cookie3=3b", "cookie4=4b"], 1226 | ); 1227 | // Agrees w/ chrome, but not FF... FF also sends cookie4=4b, but this should be 1228 | // a path-match 1229 | // fail since request-uri /foo is a *prefix* of the cookie path /foo/ 1230 | matches_are(&store, "http://bus.example.com/foo", vec!["cookie3=3b"]); 1231 | matches_are(&store, "http://bus.example.com/fo", vec![]); 1232 | matches_are(&store, "http://bus.example.com/", vec![]); 1233 | matches_are(&store, "http://bus.example.com", vec![]); 1234 | } 1235 | 1236 | #[cfg(feature = "serde_json")] 1237 | #[allow(deprecated)] 1238 | mod serde_json_tests { 1239 | use super::{CookieStore, StoreAction, add_cookie, make_match_store}; 1240 | use crate::cookie::Cookie; 1241 | use crate::CookieError; 1242 | 1243 | use crate::utils::test as test_utils; 1244 | 1245 | macro_rules! has_str { 1246 | ($e: expr, $i: ident) => {{ 1247 | let val = std::str::from_utf8(&$i[..]).unwrap(); 1248 | assert!(val.contains($e), "exp: {}\nval: {}", $e, val); 1249 | }}; 1250 | } 1251 | macro_rules! not_has_str { 1252 | ($e: expr, $i: ident) => {{ 1253 | let val = std::str::from_utf8(&$i[..]).unwrap(); 1254 | assert!(!val.contains($e), "exp: {}\nval: {}", $e, val); 1255 | }}; 1256 | } 1257 | 1258 | #[test] 1259 | fn save_json() { 1260 | let mut output = vec![]; 1261 | let mut store = CookieStore::default(); 1262 | store.save_json(&mut output).unwrap(); 1263 | assert_eq!("", std::str::from_utf8(&output[..]).unwrap()); 1264 | // non-persistent cookie, should not be saved 1265 | inserted!(add_cookie( 1266 | &mut store, 1267 | "cookie0=value0", 1268 | "http://example.com/foo/bar", 1269 | None, 1270 | None, 1271 | )); 1272 | store.save_json(&mut output).unwrap(); 1273 | assert_eq!("", std::str::from_utf8(&output[..]).unwrap()); 1274 | 1275 | // persistent cookie, Max-Age 1276 | inserted!(add_cookie( 1277 | &mut store, 1278 | "cookie1=value1", 1279 | "http://example.com/foo/bar", 1280 | None, 1281 | Some(10), 1282 | )); 1283 | store.save_json(&mut output).unwrap(); 1284 | not_has_str!("cookie0=value0", output); 1285 | has_str!("cookie1=value1", output); 1286 | output.clear(); 1287 | 1288 | // persistent cookie, Expires 1289 | inserted!(add_cookie( 1290 | &mut store, 1291 | "cookie2=value2", 1292 | "http://example.com/foo/bar", 1293 | Some(test_utils::in_days(1)), 1294 | None, 1295 | )); 1296 | store.save_json(&mut output).unwrap(); 1297 | not_has_str!("cookie0=value0", output); 1298 | has_str!("cookie1=value1", output); 1299 | has_str!("cookie2=value2", output); 1300 | output.clear(); 1301 | 1302 | inserted!(add_cookie( 1303 | &mut store, 1304 | "cookie3=value3; Domain=example.com", 1305 | "http://foo.example.com/foo/bar", 1306 | Some(test_utils::in_days(1)), 1307 | None, 1308 | )); 1309 | inserted!(add_cookie( 1310 | &mut store, 1311 | "cookie4=value4; Path=/foo/", 1312 | "http://foo.example.com/foo/bar", 1313 | Some(test_utils::in_days(1)), 1314 | None, 1315 | )); 1316 | inserted!(add_cookie( 1317 | &mut store, 1318 | "cookie5=value5", 1319 | "http://127.0.0.1/foo/bar", 1320 | Some(test_utils::in_days(1)), 1321 | None, 1322 | )); 1323 | inserted!(add_cookie( 1324 | &mut store, 1325 | "cookie6=value6", 1326 | "http://[::1]/foo/bar", 1327 | Some(test_utils::in_days(1)), 1328 | None, 1329 | )); 1330 | inserted!(add_cookie( 1331 | &mut store, 1332 | "cookie7=value7; Secure", 1333 | "https://[::1]/foo/bar", 1334 | Some(test_utils::in_days(1)), 1335 | None, 1336 | )); 1337 | inserted!(add_cookie( 1338 | &mut store, 1339 | "cookie8=value8; HttpOnly", 1340 | "http://[::1]/foo/bar", 1341 | Some(test_utils::in_days(1)), 1342 | None, 1343 | )); 1344 | store.save_json(&mut output).unwrap(); 1345 | not_has_str!("cookie0=value0", output); 1346 | has_str!("cookie1=value1", output); 1347 | has_str!("cookie2=value2", output); 1348 | has_str!("cookie3=value3", output); 1349 | has_str!("cookie4=value4", output); 1350 | has_str!("cookie5=value5", output); 1351 | has_str!("cookie6=value6", output); 1352 | has_str!("cookie7=value7; Secure", output); 1353 | has_str!("cookie8=value8; HttpOnly", output); 1354 | output.clear(); 1355 | } 1356 | 1357 | #[test] 1358 | fn serialize_json() { 1359 | let mut output = vec![]; 1360 | let mut store = CookieStore::default(); 1361 | serde_json::to_writer(&mut output, &store).unwrap(); 1362 | assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap()); 1363 | output.clear(); 1364 | 1365 | // non-persistent cookie, should not be saved 1366 | inserted!(add_cookie( 1367 | &mut store, 1368 | "cookie0=value0", 1369 | "http://example.com/foo/bar", 1370 | None, 1371 | None, 1372 | )); 1373 | serde_json::to_writer(&mut output, &store).unwrap(); 1374 | assert_eq!("[]", std::str::from_utf8(&output[..]).unwrap()); 1375 | output.clear(); 1376 | 1377 | // persistent cookie, Max-Age 1378 | inserted!(add_cookie( 1379 | &mut store, 1380 | "cookie1=value1", 1381 | "http://example.com/foo/bar", 1382 | None, 1383 | Some(10), 1384 | )); 1385 | serde_json::to_writer(&mut output, &store).unwrap(); 1386 | not_has_str!("cookie0=value0", output); 1387 | has_str!("cookie1=value1", output); 1388 | output.clear(); 1389 | 1390 | // persistent cookie, Expires 1391 | inserted!(add_cookie( 1392 | &mut store, 1393 | "cookie2=value2", 1394 | "http://example.com/foo/bar", 1395 | Some(test_utils::in_days(1)), 1396 | None, 1397 | )); 1398 | serde_json::to_writer(&mut output, &store).unwrap(); 1399 | not_has_str!("cookie0=value0", output); 1400 | has_str!("cookie1=value1", output); 1401 | has_str!("cookie2=value2", output); 1402 | output.clear(); 1403 | 1404 | inserted!(add_cookie( 1405 | &mut store, 1406 | "cookie3=value3; Domain=example.com", 1407 | "http://foo.example.com/foo/bar", 1408 | Some(test_utils::in_days(1)), 1409 | None, 1410 | )); 1411 | inserted!(add_cookie( 1412 | &mut store, 1413 | "cookie4=value4; Path=/foo/", 1414 | "http://foo.example.com/foo/bar", 1415 | Some(test_utils::in_days(1)), 1416 | None, 1417 | )); 1418 | inserted!(add_cookie( 1419 | &mut store, 1420 | "cookie5=value5", 1421 | "http://127.0.0.1/foo/bar", 1422 | Some(test_utils::in_days(1)), 1423 | None, 1424 | )); 1425 | inserted!(add_cookie( 1426 | &mut store, 1427 | "cookie6=value6", 1428 | "http://[::1]/foo/bar", 1429 | Some(test_utils::in_days(1)), 1430 | None, 1431 | )); 1432 | inserted!(add_cookie( 1433 | &mut store, 1434 | "cookie7=value7; Secure", 1435 | "https://[::1]/foo/bar", 1436 | Some(test_utils::in_days(1)), 1437 | None, 1438 | )); 1439 | inserted!(add_cookie( 1440 | &mut store, 1441 | "cookie8=value8; HttpOnly", 1442 | "http://[::1]/foo/bar", 1443 | Some(test_utils::in_days(1)), 1444 | None, 1445 | )); 1446 | serde_json::to_writer(&mut output, &store).unwrap(); 1447 | not_has_str!("cookie0=value0", output); 1448 | has_str!("cookie1=value1", output); 1449 | has_str!("cookie2=value2", output); 1450 | has_str!("cookie3=value3", output); 1451 | has_str!("cookie4=value4", output); 1452 | has_str!("cookie5=value5", output); 1453 | has_str!("cookie6=value6", output); 1454 | has_str!("cookie7=value7; Secure", output); 1455 | has_str!("cookie8=value8; HttpOnly", output); 1456 | output.clear(); 1457 | } 1458 | 1459 | #[test] 1460 | fn load_json() { 1461 | let mut store = CookieStore::default(); 1462 | // non-persistent cookie, should not be saved 1463 | inserted!(add_cookie( 1464 | &mut store, 1465 | "cookie0=value0", 1466 | "http://example.com/foo/bar", 1467 | None, 1468 | None, 1469 | )); 1470 | // persistent cookie, Max-Age 1471 | inserted!(add_cookie( 1472 | &mut store, 1473 | "cookie1=value1", 1474 | "http://example.com/foo/bar", 1475 | None, 1476 | Some(10), 1477 | )); 1478 | // persistent cookie, Expires 1479 | inserted!(add_cookie( 1480 | &mut store, 1481 | "cookie2=value2", 1482 | "http://example.com/foo/bar", 1483 | Some(test_utils::in_days(1)), 1484 | None, 1485 | )); 1486 | inserted!(add_cookie( 1487 | &mut store, 1488 | "cookie3=value3; Domain=example.com", 1489 | "http://foo.example.com/foo/bar", 1490 | Some(test_utils::in_days(1)), 1491 | None, 1492 | )); 1493 | inserted!(add_cookie( 1494 | &mut store, 1495 | "cookie4=value4; Path=/foo/", 1496 | "http://foo.example.com/foo/bar", 1497 | Some(test_utils::in_days(1)), 1498 | None, 1499 | )); 1500 | inserted!(add_cookie( 1501 | &mut store, 1502 | "cookie5=value5", 1503 | "http://127.0.0.1/foo/bar", 1504 | Some(test_utils::in_days(1)), 1505 | None, 1506 | )); 1507 | inserted!(add_cookie( 1508 | &mut store, 1509 | "cookie6=value6", 1510 | "http://[::1]/foo/bar", 1511 | Some(test_utils::in_days(1)), 1512 | None, 1513 | )); 1514 | inserted!(add_cookie( 1515 | &mut store, 1516 | "cookie7=value7; Secure", 1517 | "http://example.com/foo/bar", 1518 | Some(test_utils::in_days(1)), 1519 | None, 1520 | )); 1521 | inserted!(add_cookie( 1522 | &mut store, 1523 | "cookie8=value8; HttpOnly", 1524 | "http://example.com/foo/bar", 1525 | Some(test_utils::in_days(1)), 1526 | None, 1527 | )); 1528 | let mut output = vec![]; 1529 | store.save_json(&mut output).unwrap(); 1530 | not_has_str!("cookie0=value0", output); 1531 | has_str!("cookie1=value1", output); 1532 | has_str!("cookie2=value2", output); 1533 | has_str!("cookie3=value3", output); 1534 | has_str!("cookie4=value4", output); 1535 | has_str!("cookie5=value5", output); 1536 | has_str!("cookie6=value6", output); 1537 | has_str!("cookie7=value7; Secure", output); 1538 | has_str!("cookie8=value8; HttpOnly", output); 1539 | let store = CookieStore::load_json(&output[..]).unwrap(); 1540 | assert!(store.get("example.com", "/foo", "cookie0").is_none()); 1541 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); 1542 | assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); 1543 | assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); 1544 | assert!( 1545 | store 1546 | .get("foo.example.com", "/foo/", "cookie4") 1547 | .unwrap() 1548 | .value() 1549 | == "value4" 1550 | ); 1551 | assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); 1552 | assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); 1553 | assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); 1554 | assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); 1555 | 1556 | output.clear(); 1557 | let store = make_match_store(); 1558 | store.save_json(&mut output).unwrap(); 1559 | let store = CookieStore::load_json(&output[..]).unwrap(); 1560 | check_matches!(&store); 1561 | } 1562 | 1563 | #[test] 1564 | fn deserialize_json() { 1565 | let mut store = CookieStore::default(); 1566 | // non-persistent cookie, should not be saved 1567 | inserted!(add_cookie( 1568 | &mut store, 1569 | "cookie0=value0", 1570 | "http://example.com/foo/bar", 1571 | None, 1572 | None, 1573 | )); 1574 | // persistent cookie, Max-Age 1575 | inserted!(add_cookie( 1576 | &mut store, 1577 | "cookie1=value1", 1578 | "http://example.com/foo/bar", 1579 | None, 1580 | Some(10), 1581 | )); 1582 | // persistent cookie, Expires 1583 | inserted!(add_cookie( 1584 | &mut store, 1585 | "cookie2=value2", 1586 | "http://example.com/foo/bar", 1587 | Some(test_utils::in_days(1)), 1588 | None, 1589 | )); 1590 | inserted!(add_cookie( 1591 | &mut store, 1592 | "cookie3=value3; Domain=example.com", 1593 | "http://foo.example.com/foo/bar", 1594 | Some(test_utils::in_days(1)), 1595 | None, 1596 | )); 1597 | inserted!(add_cookie( 1598 | &mut store, 1599 | "cookie4=value4; Path=/foo/", 1600 | "http://foo.example.com/foo/bar", 1601 | Some(test_utils::in_days(1)), 1602 | None, 1603 | )); 1604 | inserted!(add_cookie( 1605 | &mut store, 1606 | "cookie5=value5", 1607 | "http://127.0.0.1/foo/bar", 1608 | Some(test_utils::in_days(1)), 1609 | None, 1610 | )); 1611 | inserted!(add_cookie( 1612 | &mut store, 1613 | "cookie6=value6", 1614 | "http://[::1]/foo/bar", 1615 | Some(test_utils::in_days(1)), 1616 | None, 1617 | )); 1618 | inserted!(add_cookie( 1619 | &mut store, 1620 | "cookie7=value7; Secure", 1621 | "http://example.com/foo/bar", 1622 | Some(test_utils::in_days(1)), 1623 | None, 1624 | )); 1625 | inserted!(add_cookie( 1626 | &mut store, 1627 | "cookie8=value8; HttpOnly", 1628 | "http://example.com/foo/bar", 1629 | Some(test_utils::in_days(1)), 1630 | None, 1631 | )); 1632 | let mut output = vec![]; 1633 | serde_json::to_writer(&mut output, &store).unwrap(); 1634 | not_has_str!("cookie0=value0", output); 1635 | has_str!("cookie1=value1", output); 1636 | has_str!("cookie2=value2", output); 1637 | has_str!("cookie3=value3", output); 1638 | has_str!("cookie4=value4", output); 1639 | has_str!("cookie5=value5", output); 1640 | has_str!("cookie6=value6", output); 1641 | has_str!("cookie7=value7; Secure", output); 1642 | has_str!("cookie8=value8; HttpOnly", output); 1643 | let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); 1644 | assert!(store.get("example.com", "/foo", "cookie0").is_none()); 1645 | assert!(store.get("example.com", "/foo", "cookie1").unwrap().value() == "value1"); 1646 | assert!(store.get("example.com", "/foo", "cookie2").unwrap().value() == "value2"); 1647 | assert!(store.get("example.com", "/foo", "cookie3").unwrap().value() == "value3"); 1648 | assert!( 1649 | store 1650 | .get("foo.example.com", "/foo/", "cookie4") 1651 | .unwrap() 1652 | .value() 1653 | == "value4" 1654 | ); 1655 | assert!(store.get("127.0.0.1", "/foo", "cookie5").unwrap().value() == "value5"); 1656 | assert!(store.get("[::1]", "/foo", "cookie6").unwrap().value() == "value6"); 1657 | assert!(store.get("example.com", "/foo", "cookie7").unwrap().value() == "value7"); 1658 | assert!(store.get("example.com", "/foo", "cookie8").unwrap().value() == "value8"); 1659 | 1660 | output.clear(); 1661 | let store = make_match_store(); 1662 | serde_json::to_writer(&mut output, &store).unwrap(); 1663 | let store: CookieStore = serde_json::from_reader(&output[..]).unwrap(); 1664 | check_matches!(&store); 1665 | } 1666 | 1667 | #[test] 1668 | fn expiry_json() { 1669 | let mut store = make_match_store(); 1670 | let request_url = test_utils::url("http://foo.example.com"); 1671 | let expired_cookie = Cookie::parse("cookie1=value1; Max-Age=-1", &request_url).unwrap(); 1672 | expired_err!(store.insert(expired_cookie, &request_url)); 1673 | check_matches!(&store); 1674 | match store.get_mut("example.com", "/", "cookie6") { 1675 | Some(cookie) => cookie.expire(), 1676 | None => unreachable!(), 1677 | } 1678 | values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); 1679 | values_are!(store, "http://example.org/foo/bar", vec!["8"]); 1680 | values_are!(store, "http://example.org/bus/bar", vec![]); 1681 | values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); 1682 | values_are!(store, "http://bar.example.org/bus/bar", vec![]); 1683 | values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); 1684 | values_are!(store, "http://example.com/sec/foo", vec!["3"]); 1685 | values_are!(store, "ftp://example.com/sec/foo", vec![]); 1686 | values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); 1687 | values_are!(store, "http://example.com/foo/bar/bus", vec!["1", "5"]); 1688 | match store.get_any("example.com", "/", "cookie6") { 1689 | Some(cookie) => assert!(cookie.is_expired()), 1690 | None => unreachable!(), 1691 | } 1692 | // inserting an expired cookie that matches an existing cookie should expire 1693 | // the existing 1694 | let request_url = test_utils::url("http://example.com/foo/"); 1695 | let expired_cookie = Cookie::parse("cookie5=value5; Max-Age=-1", &request_url).unwrap(); 1696 | expired_existing!(store.insert(expired_cookie, &request_url)); 1697 | values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); 1698 | values_are!(store, "http://example.org/foo/bar", vec!["8"]); 1699 | values_are!(store, "http://example.org/bus/bar", vec![]); 1700 | values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); 1701 | values_are!(store, "http://bar.example.org/bus/bar", vec![]); 1702 | values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); 1703 | values_are!(store, "http://example.com/sec/foo", vec!["3"]); 1704 | values_are!(store, "ftp://example.com/sec/foo", vec![]); 1705 | values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); 1706 | values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); 1707 | match store.get_any("example.com", "/foo", "cookie5") { 1708 | Some(cookie) => assert!(cookie.is_expired()), 1709 | None => unreachable!(), 1710 | } 1711 | // save and loading the store should drop any expired cookies 1712 | let mut output = vec![]; 1713 | store.save_json(&mut output).unwrap(); 1714 | store = CookieStore::load_json(&output[..]).unwrap(); 1715 | values_are!(store, "http://unknowndomain.org/foo/bar", vec![]); 1716 | values_are!(store, "http://example.org/foo/bar", vec!["8"]); 1717 | values_are!(store, "http://example.org/bus/bar", vec![]); 1718 | values_are!(store, "http://bar.example.org/foo/bar", vec!["9"]); 1719 | values_are!(store, "http://bar.example.org/bus/bar", vec![]); 1720 | values_are!(store, "https://example.com/sec/foo", vec!["4", "3", "2"]); 1721 | values_are!(store, "http://example.com/sec/foo", vec!["3"]); 1722 | values_are!(store, "ftp://example.com/sec/foo", vec![]); 1723 | values_are!(store, "http://bar.example.com/foo/bar/bus", vec!["7"]); 1724 | values_are!(store, "http://example.com/foo/bar/bus", vec!["1"]); 1725 | assert!(store.get_any("example.com", "/", "cookie6").is_none()); 1726 | assert!(store.get_any("example.com", "/foo", "cookie5").is_none()); 1727 | } 1728 | 1729 | #[test] 1730 | fn non_persistent_json() { 1731 | let mut store = make_match_store(); 1732 | check_matches!(&store); 1733 | let request_url = test_utils::url("http://example.com/tmp/"); 1734 | let non_persistent = Cookie::parse("cookie10=value10", &request_url).unwrap(); 1735 | inserted!(store.insert(non_persistent, &request_url)); 1736 | match store.get("example.com", "/tmp", "cookie10") { 1737 | None => unreachable!(), 1738 | Some(cookie) => assert_eq!("value10", cookie.value()), 1739 | } 1740 | // save and loading the store should drop any non-persistent cookies 1741 | let mut output = vec![]; 1742 | store.save_json(&mut output).unwrap(); 1743 | store = CookieStore::load_json(&output[..]).unwrap(); 1744 | check_matches!(&store); 1745 | assert!(store.get("example.com", "/tmp", "cookie10").is_none()); 1746 | assert!(store.get_any("example.com", "/tmp", "cookie10").is_none()); 1747 | } 1748 | 1749 | } 1750 | } 1751 | 1752 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | //! # cookie_store 3 | //! Provides an implementation for storing and retrieving [`Cookie`]s per the path and domain matching 4 | //! rules specified in [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). 5 | //! 6 | //! ## Example 7 | //! Please refer to the [reqwest_cookie_store](https://crates.io/crates/reqwest_cookie_store) for 8 | //! an example of using this library along with [reqwest](https://crates.io/crates/reqwest). 9 | //! 10 | //! ## Feature flags 11 | #![doc = document_features::document_features!()] 12 | 13 | use idna; 14 | 15 | pub use ::cookie::{Cookie as RawCookie, ParseError as RawCookieParseError}; 16 | 17 | mod cookie; 18 | pub use crate::cookie::Error as CookieError; 19 | pub use crate::cookie::{Cookie, CookieResult}; 20 | mod cookie_domain; 21 | pub use crate::cookie_domain::CookieDomain; 22 | mod cookie_expiration; 23 | pub use crate::cookie_expiration::CookieExpiration; 24 | mod cookie_path; 25 | pub use crate::cookie_path::CookiePath; 26 | mod cookie_store; 27 | pub use crate::cookie_store::{CookieStore, StoreAction}; 28 | #[cfg(feature = "serde")] 29 | pub mod serde; 30 | mod utils; 31 | 32 | #[derive(Debug)] 33 | pub struct IdnaErrors(idna::Errors); 34 | 35 | impl std::fmt::Display for IdnaErrors { 36 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 37 | write!(f, "IDNA errors: {:#?}", self.0) 38 | } 39 | } 40 | 41 | impl std::error::Error for IdnaErrors {} 42 | 43 | impl From for IdnaErrors { 44 | fn from(e: idna::Errors) -> Self { 45 | IdnaErrors(e) 46 | } 47 | } 48 | 49 | pub type Error = Box; 50 | pub type Result = std::result::Result; 51 | 52 | #[cfg(feature = "serde")] 53 | pub(crate) mod rfc3339_fmt { 54 | 55 | pub(crate) const RFC3339_FORMAT: &[time::format_description::FormatItem] = 56 | time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"); 57 | 58 | pub(super) fn serialize(t: &time::OffsetDateTime, serializer: S) -> Result 59 | where 60 | S: serde::Serializer, 61 | { 62 | use serde::ser::Error; 63 | // An explicit format string is used here, instead of time::format_description::well_known::Rfc3339, to explicitly 64 | // utilize the 'Z' terminator instead of +00:00 format for Zulu time. 65 | let s = t.format(&RFC3339_FORMAT).map_err(|e| { 66 | println!("{}", e); 67 | S::Error::custom(format!( 68 | "Could not parse datetime '{}' as RFC3339 UTC format: {}", 69 | t, e 70 | )) 71 | })?; 72 | serializer.serialize_str(&s) 73 | } 74 | 75 | pub(super) fn deserialize<'de, D>(t: D) -> Result 76 | where 77 | D: serde::Deserializer<'de>, 78 | { 79 | use serde::{de::Error, Deserialize}; 80 | 81 | let s = String::deserialize(t)?; 82 | time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err( 83 | |e| { 84 | D::Error::custom(format!( 85 | "Could not parse string '{}' as RFC3339 UTC format: {}", 86 | s, e 87 | )) 88 | }, 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/serde.rs: -------------------------------------------------------------------------------- 1 | //! De/serialization functionality 2 | //! Requires feature `serde` 3 | 4 | use std::io::{BufRead, Write}; 5 | 6 | use crate::{Cookie, cookie_store::StoreResult, CookieStore}; 7 | 8 | #[cfg(feature = "serde_json")] 9 | pub mod json; 10 | #[cfg(feature = "serde_ron")] 11 | pub mod ron; 12 | 13 | /// Load cookies from `reader`, deserializing with `cookie_from_str`, skipping any __expired__ 14 | /// cookies 15 | pub fn load(reader: R, cookies_from_str: F) -> StoreResult 16 | where 17 | R: BufRead, 18 | F: Fn(&str) -> Result>, E>, 19 | crate::Error: From, 20 | { 21 | load_from(reader, cookies_from_str, false) 22 | } 23 | 24 | /// Load cookies from `reader`, deserializing with `cookie_from_str`, loading both __unexpired__ 25 | /// and __expired__ cookies 26 | pub fn load_all(reader: R, cookies_from_str: F) -> StoreResult 27 | where 28 | R: BufRead, 29 | F: Fn(&str) -> Result>, E>, 30 | crate::Error: From, 31 | { 32 | load_from(reader, cookies_from_str, true) 33 | } 34 | 35 | fn load_from( 36 | mut reader: R, 37 | cookies_from_str: F, 38 | include_expired: bool, 39 | ) -> StoreResult 40 | where 41 | R: BufRead, 42 | F: Fn(&str) -> Result>, E>, 43 | crate::Error: From, 44 | { 45 | let mut cookie_store = String::new(); 46 | reader.read_to_string(&mut cookie_store)?; 47 | let cookies = cookies_from_str(&cookie_store)?; 48 | CookieStore::from_cookies( 49 | cookies.into_iter().map(|cookies| Ok(cookies)), 50 | include_expired, 51 | ) 52 | } 53 | 54 | /// Serialize any __unexpired__ and __persistent__ cookies in the store with `cookie_to_string` 55 | /// and write them to `writer` 56 | pub fn save( 57 | cookie_store: &CookieStore, 58 | writer: &mut W, 59 | cookies_to_string: F, 60 | ) -> StoreResult<()> 61 | where 62 | W: Write, 63 | F: Fn(&Vec>) -> Result, 64 | crate::Error: From, 65 | { 66 | let mut cookies = Vec::new(); 67 | for cookie in cookie_store.iter_unexpired() { 68 | if cookie.is_persistent() { 69 | cookies.push(cookie.clone()); 70 | } 71 | } 72 | let cookies = cookies_to_string(&cookies); 73 | writeln!(writer, "{}", cookies?)?; 74 | Ok(()) 75 | } 76 | 77 | /// Serialize all (including __expired__ and __non-persistent__) cookies in the store with `cookie_to_string` and write them to `writer` 78 | pub fn save_incl_expired_and_nonpersistent( 79 | cookie_store: &CookieStore, 80 | writer: &mut W, 81 | cookies_to_string: F, 82 | ) -> StoreResult<()> 83 | where 84 | W: Write, 85 | F: Fn(&Vec>) -> Result, 86 | crate::Error: From, 87 | { 88 | let mut cookies = Vec::new(); 89 | for cookie in cookie_store.iter_any() { 90 | cookies.push(cookie.clone()); 91 | } 92 | let cookies = cookies_to_string(&cookies); 93 | writeln!(writer, "{}", cookies?)?; 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /src/serde/json.rs: -------------------------------------------------------------------------------- 1 | //! De/serialization via the JSON format 2 | //! Requires feature `serde_json` 3 | 4 | use std::io::{BufRead, Write}; 5 | 6 | use crate::cookie_store::{StoreResult, CookieStore}; 7 | 8 | /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies. 9 | /// __NB__: This function is not compatible with data produced by [CookieStore::save_json] or 10 | /// [CookieStore::save_incl_expired_and_nonpersistent_json]. 11 | pub fn load(reader: R) -> StoreResult { 12 | super::load(reader, |cookies| serde_json::from_str(cookies)) 13 | } 14 | 15 | /// Load JSON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies. 16 | /// __NB__: This function is not compatible with data produced by [CookieStore::save_json] or 17 | /// [CookieStore::save_incl_expired_and_nonpersistent_json]. 18 | pub fn load_all(reader: R) -> StoreResult { 19 | super::load_all(reader, |cookies| serde_json::from_str(cookies)) 20 | } 21 | 22 | /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and 23 | /// write them to `writer`. 24 | /// __NB__: This function does not produce data compatible with [CookieStore::load_json] or 25 | /// [CookieStore::load_json_all]. 26 | pub fn save(cookie_store: &CookieStore, writer: &mut W) -> StoreResult<()> { 27 | super::save(cookie_store, writer, ::serde_json::to_string_pretty) 28 | } 29 | 30 | /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to JSON format and write them to `writer`. 31 | /// __NB__: This function does not produce data compatible with [CookieStore::load_json] or 32 | /// [CookieStore::load_json_all]. 33 | pub fn save_incl_expired_and_nonpersistent( 34 | cookie_store: &CookieStore, 35 | writer: &mut W, 36 | ) -> StoreResult<()> { 37 | super::save_incl_expired_and_nonpersistent(cookie_store, writer, ::serde_json::to_string_pretty) 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use std::io::BufWriter; 43 | 44 | use super::{ save_incl_expired_and_nonpersistent, save }; 45 | 46 | use super::{ load, load_all }; 47 | 48 | fn cookie() -> String { 49 | r#"[ 50 | { 51 | "raw_cookie": "2=two; SameSite=None; Secure; Path=/; Expires=Tue, 03 Aug 2100 00:38:37 GMT", 52 | "path": [ 53 | "/", 54 | true 55 | ], 56 | "domain": { 57 | "HostOnly": "test.com" 58 | }, 59 | "expires": { 60 | "AtUtc": "2100-08-03T00:38:37Z" 61 | } 62 | } 63 | ] 64 | "# 65 | .to_string() 66 | } 67 | 68 | fn cookie_expired() -> String { 69 | r#"[ 70 | { 71 | "raw_cookie": "1=one; SameSite=None; Secure; Path=/; Expires=Thu, 03 Aug 2000 00:38:37 GMT", 72 | "path": [ 73 | "/", 74 | true 75 | ], 76 | "domain": { 77 | "HostOnly": "test.com" 78 | }, 79 | "expires": { 80 | "AtUtc": "2000-08-03T00:38:37Z" 81 | } 82 | } 83 | ] 84 | "# 85 | .to_string() 86 | } 87 | 88 | #[test] 89 | fn check_count() { 90 | let cookie = cookie(); 91 | 92 | let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 93 | assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 1); 94 | assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 1); 95 | 96 | let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 97 | assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); 98 | assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 1); 99 | 100 | let mut writer = BufWriter::new(Vec::new()); 101 | save(&cookie_store, &mut writer).unwrap(); 102 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 103 | assert_eq!(cookie, string); 104 | 105 | let mut writer = BufWriter::new(Vec::new()); 106 | save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); 107 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 108 | assert_eq!(cookie, string); 109 | 110 | let mut writer = BufWriter::new(Vec::new()); 111 | save(&cookie_store_all, &mut writer).unwrap(); 112 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 113 | assert_eq!(cookie, string); 114 | 115 | let mut writer = BufWriter::new(Vec::new()); 116 | save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); 117 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 118 | assert_eq!(cookie, string); 119 | } 120 | 121 | #[test] 122 | fn check_count_expired() { 123 | let cookie = cookie_expired(); 124 | 125 | let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 126 | assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 0); 127 | assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 0); 128 | 129 | let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 130 | assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); 131 | assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 0); 132 | 133 | let mut writer = BufWriter::new(Vec::new()); 134 | save(&cookie_store, &mut writer).unwrap(); 135 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 136 | assert_eq!("[]\n", string); 137 | 138 | let mut writer = BufWriter::new(Vec::new()); 139 | save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); 140 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 141 | assert_eq!("[]\n", string); 142 | 143 | let mut writer = BufWriter::new(Vec::new()); 144 | save(&cookie_store_all, &mut writer).unwrap(); 145 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 146 | assert_eq!("[]\n", string); 147 | 148 | let mut writer = BufWriter::new(Vec::new()); 149 | save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); 150 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 151 | assert_eq!(cookie, string); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/serde/ron.rs: -------------------------------------------------------------------------------- 1 | //! De/serialization via the RON format 2 | //! Requires feature `serde_ron` 3 | 4 | use std::io::{BufRead, Write}; 5 | 6 | use crate::cookie_store::{StoreResult, CookieStore}; 7 | 8 | /// Load RON-formatted cookies from `reader`, skipping any __expired__ cookies 9 | pub fn load(reader: R) -> StoreResult { 10 | super::load(reader, |cookies| ron::from_str(cookies)) 11 | } 12 | 13 | /// Load RON-formatted cookies from `reader`, loading both __expired__ and __unexpired__ cookies 14 | pub fn load_all(reader: R) -> StoreResult { 15 | super::load_all(reader, |cookies| ron::from_str(cookies)) 16 | } 17 | 18 | /// Serialize any __unexpired__ and __persistent__ cookies in the store to JSON format and 19 | /// write them to `writer` 20 | pub fn save(cookie_store: &CookieStore, writer: &mut W) -> StoreResult<()> { 21 | super::save(cookie_store, writer, |string| { 22 | ::ron::ser::to_string_pretty(string, ron::ser::PrettyConfig::default()) 23 | }) 24 | } 25 | 26 | /// Serialize all (including __expired__ and __non-persistent__) cookies in the store to RON format and write them to `writer` 27 | pub fn save_incl_expired_and_nonpersistent( 28 | cookie_store: &CookieStore, 29 | writer: &mut W, 30 | ) -> StoreResult<()> { 31 | super::save_incl_expired_and_nonpersistent(cookie_store, writer, |string| { 32 | ::ron::ser::to_string_pretty(string, ron::ser::PrettyConfig::default()) 33 | }) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use std::io::BufWriter; 39 | 40 | use super::{load, load_all}; 41 | use super::{ save_incl_expired_and_nonpersistent, save }; 42 | 43 | fn cookie() -> String { 44 | r#"[ 45 | ( 46 | raw_cookie: "2=two; SameSite=None; Secure; Path=/; Expires=Tue, 03 Aug 2100 00:38:37 GMT", 47 | path: ("/", true), 48 | domain: HostOnly("test.com"), 49 | expires: AtUtc("2100-08-03T00:38:37Z"), 50 | ), 51 | ] 52 | "#.to_string() 53 | } 54 | 55 | fn cookie_expired() -> String { 56 | r#"[ 57 | ( 58 | raw_cookie: "1=one; SameSite=None; Secure; Path=/; Expires=Thu, 03 Aug 2000 00:38:37 GMT", 59 | path: ("/", true), 60 | domain: HostOnly("test.com"), 61 | expires: AtUtc("2000-08-03T00:38:37Z"), 62 | ), 63 | ] 64 | "#.to_string() 65 | } 66 | 67 | #[test] 68 | fn check_count() { 69 | let cookie = cookie(); 70 | 71 | let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 72 | assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 1); 73 | assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 1); 74 | 75 | let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 76 | assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); 77 | assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 1); 78 | 79 | let mut writer = BufWriter::new(Vec::new()); 80 | save(&cookie_store, &mut writer).unwrap(); 81 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 82 | assert_eq!(cookie, string); 83 | 84 | let mut writer = BufWriter::new(Vec::new()); 85 | save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); 86 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 87 | assert_eq!(cookie, string); 88 | 89 | let mut writer = BufWriter::new(Vec::new()); 90 | save(&cookie_store_all, &mut writer).unwrap(); 91 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 92 | assert_eq!(cookie, string); 93 | 94 | let mut writer = BufWriter::new(Vec::new()); 95 | save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); 96 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 97 | assert_eq!(cookie, string); 98 | } 99 | 100 | #[test] 101 | fn check_count_expired() { 102 | let cookie = cookie_expired(); 103 | 104 | let cookie_store = load(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 105 | assert_eq!(cookie_store.iter_any().map(|_| 1).sum::(), 0); 106 | assert_eq!(cookie_store.iter_unexpired().map(|_| 1).sum::(), 0); 107 | 108 | let cookie_store_all = load_all(Into::<&[u8]>::into(cookie.as_bytes())).unwrap(); 109 | assert_eq!(cookie_store_all.iter_any().map(|_| 1).sum::(), 1); 110 | assert_eq!(cookie_store_all.iter_unexpired().map(|_| 1).sum::(), 0); 111 | 112 | let mut writer = BufWriter::new(Vec::new()); 113 | save(&cookie_store, &mut writer).unwrap(); 114 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 115 | assert_eq!("[]\n", string); 116 | 117 | let mut writer = BufWriter::new(Vec::new()); 118 | save_incl_expired_and_nonpersistent(&cookie_store, &mut writer).unwrap(); 119 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 120 | assert_eq!("[]\n", string); 121 | 122 | let mut writer = BufWriter::new(Vec::new()); 123 | save(&cookie_store_all, &mut writer).unwrap(); 124 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 125 | assert_eq!("[]\n", string); 126 | 127 | let mut writer = BufWriter::new(Vec::new()); 128 | save_incl_expired_and_nonpersistent(&cookie_store_all, &mut writer).unwrap(); 129 | let string = String::from_utf8(writer.into_inner().unwrap()).unwrap(); 130 | assert_eq!(cookie, string); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, Ipv6Addr}; 2 | use url::Url; 3 | use url::{Host, ParseError as UrlError}; 4 | 5 | pub trait IntoUrl { 6 | fn into_url(self) -> Result; 7 | } 8 | 9 | impl IntoUrl for Url { 10 | fn into_url(self) -> Result { 11 | Ok(self) 12 | } 13 | } 14 | 15 | impl<'a> IntoUrl for &'a str { 16 | fn into_url(self) -> Result { 17 | Url::parse(self) 18 | } 19 | } 20 | 21 | impl<'a> IntoUrl for &'a String { 22 | fn into_url(self) -> Result { 23 | Url::parse(self) 24 | } 25 | } 26 | 27 | pub fn is_http_scheme(url: &Url) -> bool { 28 | url.scheme().starts_with("http") 29 | } 30 | 31 | pub fn is_host_name(host: &str) -> bool { 32 | host.parse::().is_err() && host.parse::().is_err() 33 | } 34 | 35 | pub fn is_secure(url: &Url) -> bool { 36 | if url.scheme() == "https" { 37 | return true; 38 | } 39 | if let Some(u) = url.host() { 40 | match u { 41 | Host::Domain(d) => d == "localhost", 42 | Host::Ipv4(ip) => ip.is_loopback(), 43 | Host::Ipv6(ip) => ip.is_loopback(), 44 | } 45 | } else { 46 | false 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | pub mod test { 52 | use crate::cookie::Cookie; 53 | use time::{Duration, OffsetDateTime}; 54 | use url::Url; 55 | #[inline] 56 | pub fn url(url: &str) -> Url { 57 | Url::parse(url).unwrap() 58 | } 59 | #[inline] 60 | pub fn make_cookie<'a>( 61 | cookie: &str, 62 | url_str: &str, 63 | expires: Option, 64 | max_age: Option, 65 | ) -> Cookie<'a> { 66 | Cookie::parse( 67 | format!( 68 | "{}{}{}", 69 | cookie, 70 | expires.map_or(String::from(""), |e| format!( 71 | "; Expires={}", 72 | e.format(time::macros::format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT")).unwrap() 73 | )), 74 | max_age.map_or(String::from(""), |m| format!("; Max-Age={}", m)) 75 | ), 76 | &url(url_str), 77 | ) 78 | .unwrap() 79 | } 80 | #[inline] 81 | pub fn in_days(days: i64) -> OffsetDateTime { 82 | OffsetDateTime::now_utc() + Duration::days(days) 83 | } 84 | #[inline] 85 | pub fn in_minutes(mins: i64) -> OffsetDateTime { 86 | OffsetDateTime::now_utc() + Duration::minutes(mins) 87 | } 88 | } 89 | --------------------------------------------------------------------------------