├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── src ├── auth │ ├── authentication_scheme.rs │ ├── authorization.rs │ ├── basic_auth.rs │ ├── mod.rs │ └── www_authenticate.rs ├── body.rs ├── cache │ ├── age.rs │ ├── cache_control │ │ ├── cache_control.rs │ │ ├── cache_directive.rs │ │ └── mod.rs │ ├── clear_site_data │ │ ├── directive.rs │ │ └── mod.rs │ ├── expires.rs │ └── mod.rs ├── conditional │ ├── etag.rs │ ├── if_match.rs │ ├── if_modified_since.rs │ ├── if_none_match.rs │ ├── if_unmodified_since.rs │ ├── last_modified.rs │ ├── mod.rs │ └── vary.rs ├── content │ ├── accept.rs │ ├── accept_encoding.rs │ ├── content_encoding.rs │ ├── content_length.rs │ ├── content_location.rs │ ├── content_type.rs │ ├── encoding.rs │ ├── encoding_proposal.rs │ ├── media_type_proposal.rs │ └── mod.rs ├── error.rs ├── extensions.rs ├── headers │ ├── constants.rs │ ├── header.rs │ ├── header_name.rs │ ├── header_value.rs │ ├── header_values.rs │ ├── headers.rs │ ├── into_iter.rs │ ├── iter.rs │ ├── iter_mut.rs │ ├── mod.rs │ ├── names.rs │ ├── to_header_values.rs │ └── values.rs ├── hyperium_http.rs ├── lib.rs ├── macros.rs ├── method.rs ├── mime │ ├── constants.rs │ ├── mod.rs │ └── parse.rs ├── other │ ├── date.rs │ ├── expect.rs │ ├── mod.rs │ ├── referer.rs │ ├── retry_after.rs │ └── source_map.rs ├── parse_utils.rs ├── proxies │ ├── forwarded.rs │ └── mod.rs ├── request.rs ├── response.rs ├── security │ ├── csp.rs │ ├── mod.rs │ ├── strict_transport_security.rs │ └── timing_allow_origin.rs ├── server │ ├── allow.rs │ └── mod.rs ├── status.rs ├── status_code.rs ├── trace │ ├── mod.rs │ ├── server_timing │ │ ├── metric.rs │ │ ├── mod.rs │ │ └── parse.rs │ └── trace_context.rs ├── trailers.rs ├── transfer │ ├── encoding.rs │ ├── encoding_proposal.rs │ ├── mod.rs │ ├── te.rs │ └── transfer_encoding.rs ├── upgrade │ ├── connection.rs │ ├── mod.rs │ ├── receiver.rs │ └── sender.rs ├── utils │ ├── date.rs │ └── mod.rs └── version.rs └── tests ├── error.rs ├── fixtures ├── empty.custom ├── index.html ├── nori.png └── unknown.custom ├── headers.rs ├── mime.rs ├── querystring.rs ├── req_res_body.rs └── security.rs /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, 10 | education, socio-economic status, nationality, personal appearance, race, 11 | religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at yoshuawuyts@gmail.com, or through 61 | IRC. All complaints will be reviewed and investigated and will result in a 62 | response that is deemed necessary and appropriate to the circumstances. The 63 | project team is obligated to maintain confidentiality with regard to the 64 | reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 74 | available at 75 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions include code, documentation, answering user questions, running the 3 | project's infrastructure, and advocating for all types of users. 4 | 5 | The project welcomes all contributions from anyone willing to work in good faith 6 | with other contributors and the community. No contribution is too small and all 7 | contributions are valued. 8 | 9 | This guide explains the process for contributing to the project's GitHub 10 | Repository. 11 | 12 | - [Code of Conduct](#code-of-conduct) 13 | - [Bad Actors](#bad-actors) 14 | 15 | ## Code of Conduct 16 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* 17 | contributors are expected to follow. This code describes the *minimum* behavior 18 | expectations for all contributors. 19 | 20 | As a contributor, how you choose to act and interact towards your 21 | fellow contributors, as well as to the community, will reflect back not only 22 | on yourself but on the project as a whole. The Code of Conduct is designed and 23 | intended, above all else, to help establish a culture within the project that 24 | allows anyone and everyone who wants to contribute to feel safe doing so. 25 | 26 | Should any individual act in any way that is considered in violation of the 27 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 28 | possible, however, for any individual to *act* in such a manner that is not in 29 | violation of the strict letter of the Code of Conduct guidelines while still 30 | going completely against the spirit of what that Code is intended to accomplish. 31 | 32 | Open, diverse, and inclusive communities live and die on the basis of trust. 33 | Contributors can disagree with one another so long as they trust that those 34 | disagreements are in good faith and everyone is working towards a common 35 | goal. 36 | 37 | ## Bad Actors 38 | All contributors to tacitly agree to abide by both the letter and 39 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 40 | unwillingness, to do so will result in contributions being respectfully 41 | declined. 42 | 43 | A *bad actor* is someone who repeatedly violates the *spirit* of the Code of 44 | Conduct through consistent failure to self-regulate the way in which they 45 | interact with other contributors in the project. In doing so, bad actors 46 | alienate other contributors, discourage collaboration, and generally reflect 47 | poorly on the project as a whole. 48 | 49 | Being a bad actor may be intentional or unintentional. Typically, unintentional 50 | bad behavior can be easily corrected by being quick to apologize and correct 51 | course *even if you are not entirely convinced you need to*. Giving other 52 | contributors the benefit of the doubt and having a sincere willingness to admit 53 | that you *might* be wrong is critical for any successful open collaboration. 54 | 55 | Don't be a bad actor. 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - staging 9 | - trying 10 | 11 | jobs: 12 | build_and_test: 13 | name: Build and test 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macOS-latest] 18 | rust: [stable, nightly] 19 | 20 | steps: 21 | - uses: actions/checkout@master 22 | 23 | - name: Install ${{ matrix.rust }} toolchain 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | override: true 28 | 29 | - name: check 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: check 33 | args: --workspace --all-targets --features hyperium_http 34 | 35 | - name: check unstable 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: check 39 | args: --workspace --all-targets --features "hyperium_http,unstable" 40 | 41 | - name: tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --workspace --features "hyperium_http,unstable" 46 | 47 | check_fmt_clippy_docs: 48 | name: Checking fmt, clippy, and docs 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@master 52 | 53 | - name: Install nightly toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | profile: minimal 57 | toolchain: nightly 58 | override: true 59 | components: clippy, rustfmt 60 | 61 | - name: clippy 62 | run: cargo clippy --workspace --all-targets --features "hyperium_http,unstable" 63 | 64 | - name: fmt 65 | run: cargo fmt --all -- --check 66 | 67 | - name: docs 68 | run: cargo doc --no-deps 69 | 70 | check_wasm: 71 | name: Check wasm targets 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - uses: actions/checkout@master 76 | 77 | - name: Install nightly with wasm32-unknown-unknown 78 | uses: actions-rs/toolchain@v1 79 | with: 80 | toolchain: nightly 81 | target: wasm32-unknown-unknown 82 | override: true 83 | 84 | - name: check 85 | run: cargo check --target wasm32-unknown-unknown --no-default-features --features=fs,serde 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | target/ 3 | tmp/ 4 | dist/ 5 | npm-debug.log* 6 | Cargo.lock 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http-types" 3 | version = "3.0.0" 4 | license = "MIT OR Apache-2.0" 5 | repository = "https://github.com/http-rs/http-types" 6 | documentation = "https://docs.rs/http-types" 7 | description = "Common types for HTTP operations." 8 | keywords = ["http", "types", "request", "response", "h2"] 9 | categories = ["asynchronous", "web-programming", "web-programming::http-client", "web-programming::http-server", "web-programming::websocket"] 10 | authors = ["Yoshua Wuyts "] 11 | readme = "README.md" 12 | edition = "2018" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["docs"] 16 | rustdoc-args = ["--cfg", "feature=\"docs\""] 17 | 18 | [features] 19 | default = ["fs", "cookie-secure", "serde"] 20 | docs = ["unstable"] 21 | unstable = [] 22 | hyperium_http = ["http"] 23 | async_std = ["fs"] 24 | cookies = ["cookie"] 25 | cookie-secure = ["cookies", "cookie/secure"] 26 | fs = ["async-std"] 27 | serde = ["serde_qs", "serde_crate", "serde_json", "serde_urlencoded", "url/serde"] 28 | 29 | [dependencies] 30 | fastrand = "1.4.0" 31 | base64 = "0.13.0" 32 | futures-lite = "1.11.1" 33 | async-channel = "1.5.1" 34 | infer = "0.7.0" 35 | pin-project-lite = "0.2.0" 36 | url = "2.1.1" 37 | anyhow = "1.0.26" 38 | 39 | # features: async_std 40 | async-std = { version = "1.6.0", optional = true } 41 | 42 | # features: hyperium/http 43 | http = { version = "0.2.0", optional = true } 44 | 45 | # features: cookies 46 | cookie = { version = "0.16.0", features = ["percent-encode"], optional = true } 47 | 48 | # features: serde 49 | serde_json = { version = "1.0.51", optional = true } 50 | serde_crate = { version = "1.0.106", features = ["derive"], optional = true, package = "serde" } 51 | serde_urlencoded = { version = "0.7.0", optional = true} 52 | serde_qs = { version = "0.9.1", optional = true } 53 | 54 | 55 | [dev-dependencies] 56 | http = "0.2.0" 57 | async-std = { version = "1.6.0", features = ["attributes"] } 58 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Yoshua Wuyts 4 | Copyright (c) 2017 http-rs authors 5 | Copyright (c) 2020 Jacob Brown 6 | Copyright (c) 2016-2018 Michael Tilli (Pyfisch) & `httpdate` contributors 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

http-types

2 |
3 | 4 | Common types for HTTP operations. 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 |
27 | 28 |
29 |

30 | 31 | API Docs 32 | 33 | | 34 | 35 | Releases 36 | 37 | | 38 | 39 | Contributing 40 | 41 |

42 |
43 | 44 | ## Installation 45 | ```sh 46 | $ cargo add http-types 47 | ``` 48 | 49 | ## Safety 50 | This crate uses `unsafe` in a few places to convert from validated ASCII byte 51 | buffers to utf-8 strings. 52 | 53 | ## Contributing 54 | Want to join us? Check out our ["Contributing" guide][contributing] and take a 55 | look at some of these issues: 56 | 57 | - [Issues labeled "good first issue"][good-first-issue] 58 | - [Issues labeled "help wanted"][help-wanted] 59 | 60 | [contributing]: https://github.com/http-rs/http-types/blob/main/.github/CONTRIBUTING.md 61 | [good-first-issue]: https://github.com/http-rs/http-types/labels/good%20first%20issue 62 | [help-wanted]: https://github.com/http-rs/http-types/labels/help%20wanted 63 | 64 | ## License 65 | 66 | 67 | Licensed under either of Apache License, Version 68 | 2.0 or MIT license at your option. 69 | 70 | 71 |
72 | 73 | 74 | Unless you explicitly state otherwise, any contribution intentionally submitted 75 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 76 | be dual licensed as above, without any additional terms or conditions. 77 | 78 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::path::Path; 4 | use std::process::{Command, ExitStatus, Stdio}; 5 | 6 | // This build script is copied from 7 | // [anyhow](https://github.com/dtolnay/anyhow/blob/master/build.rs), 8 | // and is a type of feature detection to determine if the current rust 9 | // toolchain has backtraces available. 10 | // 11 | // It exercises the surface area that we expect of the std Backtrace 12 | // type. If the current toolchain is able to compile it, we enable a 13 | // backtrace compiler configuration flag in http-types. We then 14 | // conditionally require the compiler feature in src/lib.rs with 15 | // `#![cfg_attr(backtrace, feature(backtrace))]` 16 | // and gate our backtrace code behind `#[cfg(backtrace)]` 17 | 18 | const PROBE: &str = r#" 19 | #![feature(backtrace)] 20 | #![allow(dead_code)] 21 | 22 | use std::backtrace::{Backtrace, BacktraceStatus}; 23 | use std::error::Error; 24 | use std::fmt::{self, Display}; 25 | 26 | #[derive(Debug)] 27 | struct E; 28 | 29 | impl Display for E { 30 | fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { 31 | unimplemented!() 32 | } 33 | } 34 | 35 | impl Error for E { 36 | fn backtrace(&self) -> Option<&Backtrace> { 37 | let backtrace = Backtrace::capture(); 38 | match backtrace.status() { 39 | BacktraceStatus::Captured | BacktraceStatus::Disabled | _ => {} 40 | } 41 | unimplemented!() 42 | } 43 | } 44 | "#; 45 | 46 | fn main() { 47 | match compile_probe() { 48 | Some(status) if status.success() => println!("cargo:rustc-cfg=backtrace"), 49 | _ => {} 50 | } 51 | } 52 | 53 | fn compile_probe() -> Option { 54 | let rustc = env::var_os("RUSTC")?; 55 | let out_dir = env::var_os("OUT_DIR")?; 56 | let probefile = Path::new(&out_dir).join("probe.rs"); 57 | fs::write(&probefile, PROBE).ok()?; 58 | Command::new(rustc) 59 | .stderr(Stdio::null()) 60 | .arg("--edition=2018") 61 | .arg("--crate-name=http_types_build") 62 | .arg("--crate-type=lib") 63 | .arg("--emit=metadata") 64 | .arg("--out-dir") 65 | .arg(out_dir) 66 | .arg(probefile) 67 | .status() 68 | .ok() 69 | } 70 | -------------------------------------------------------------------------------- /src/auth/authentication_scheme.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | use std::str::FromStr; 3 | 4 | use crate::bail_status as bail; 5 | 6 | /// HTTP Mutual Authentication Algorithms 7 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 8 | #[non_exhaustive] 9 | pub enum AuthenticationScheme { 10 | /// [RFC7617](https://tools.ietf.org/html/rfc7617) Basic auth 11 | Basic, 12 | /// [RFC6750](https://tools.ietf.org/html/rfc6750) Bearer auth 13 | Bearer, 14 | /// [RFC7616](https://tools.ietf.org/html/rfc7616) Digest auth 15 | Digest, 16 | /// [RFC7486](https://tools.ietf.org/html/rfc7486) HTTP Origin-Bound Authentication (HOBA) 17 | Hoba, 18 | /// [RFC8120](https://tools.ietf.org/html/rfc8120) Mutual auth 19 | Mutual, 20 | /// [RFC4559](https://tools.ietf.org/html/rfc4559) Negotiate auth 21 | Negotiate, 22 | /// [RFC5849](https://tools.ietf.org/html/rfc5849) OAuth 23 | OAuth, 24 | /// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA1 auth 25 | ScramSha1, 26 | /// [RFC7804](https://tools.ietf.org/html/rfc7804) SCRAM SHA256 auth 27 | ScramSha256, 28 | /// [RFC8292](https://tools.ietf.org/html/rfc8292) Vapid auth 29 | Vapid, 30 | } 31 | 32 | impl Display for AuthenticationScheme { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | match self { 35 | Self::Basic => write!(f, "Basic"), 36 | Self::Bearer => write!(f, "Bearer"), 37 | Self::Digest => write!(f, "Digest"), 38 | Self::Hoba => write!(f, "HOBA"), 39 | Self::Mutual => write!(f, "Mutual"), 40 | Self::Negotiate => write!(f, "Negotiate"), 41 | Self::OAuth => write!(f, "OAuth"), 42 | Self::ScramSha1 => write!(f, "SCRAM-SHA-1"), 43 | Self::ScramSha256 => write!(f, "SCRAM-SHA-256"), 44 | Self::Vapid => write!(f, "vapid"), 45 | } 46 | } 47 | } 48 | 49 | impl FromStr for AuthenticationScheme { 50 | type Err = crate::Error; 51 | 52 | fn from_str(s: &str) -> Result { 53 | // NOTE(yosh): matching here is lowercase as specified by RFC2617#section-1.2 54 | // > [...] case-insensitive token to identify the authentication scheme [...] 55 | // https://tools.ietf.org/html/rfc2617#section-1.2 56 | #[allow(clippy::match_str_case_mismatch)] 57 | match s.to_lowercase().as_str() { 58 | "basic" => Ok(Self::Basic), 59 | "bearer" => Ok(Self::Bearer), 60 | "digest" => Ok(Self::Digest), 61 | "hoba" => Ok(Self::Hoba), 62 | "mutual" => Ok(Self::Mutual), 63 | "negotiate" => Ok(Self::Negotiate), 64 | "oauth" => Ok(Self::OAuth), 65 | "scram-sha-1" => Ok(Self::ScramSha1), 66 | "scram-sha-256" => Ok(Self::ScramSha256), 67 | "vapid" => Ok(Self::Vapid), 68 | s => bail!(400, "`{}` is not a recognized authentication scheme", s), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/auth/authorization.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::AuthenticationScheme; 2 | use crate::bail_status as bail; 3 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, AUTHORIZATION}; 4 | 5 | /// Credentials to authenticate a user agent with a server. 6 | /// 7 | /// # Specifications 8 | /// 9 | /// - [RFC 7235, section 4.2: Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// # fn main() -> http_types::Result<()> { 15 | /// # 16 | /// use http_types::Response; 17 | /// use http_types::auth::{AuthenticationScheme, Authorization}; 18 | /// 19 | /// let scheme = AuthenticationScheme::Basic; 20 | /// let credentials = "0xdeadbeef202020"; 21 | /// let authz = Authorization::new(scheme, credentials.into()); 22 | /// 23 | /// let mut res = Response::new(200); 24 | /// res.insert_header(&authz, &authz); 25 | /// 26 | /// let authz = Authorization::from_headers(res)?.unwrap(); 27 | /// 28 | /// assert_eq!(authz.scheme(), AuthenticationScheme::Basic); 29 | /// assert_eq!(authz.credentials(), credentials); 30 | /// # 31 | /// # Ok(()) } 32 | /// ``` 33 | #[derive(Debug)] 34 | pub struct Authorization { 35 | scheme: AuthenticationScheme, 36 | credentials: String, 37 | } 38 | 39 | impl Authorization { 40 | /// Create a new instance of `Authorization`. 41 | pub fn new(scheme: AuthenticationScheme, credentials: String) -> Self { 42 | Self { 43 | scheme, 44 | credentials, 45 | } 46 | } 47 | 48 | /// Create a new instance from headers. 49 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 50 | let headers = match headers.as_ref().get(AUTHORIZATION) { 51 | Some(headers) => headers, 52 | None => return Ok(None), 53 | }; 54 | 55 | // If we successfully parsed the header then there's always at least one 56 | // entry. We want the last entry. 57 | let value = headers.iter().last().unwrap(); 58 | 59 | let mut iter = value.as_str().splitn(2, ' '); 60 | let scheme = iter.next(); 61 | let credential = iter.next(); 62 | let (scheme, credentials) = match (scheme, credential) { 63 | (None, _) => bail!(400, "Could not find scheme"), 64 | (Some(_), None) => bail!(400, "Could not find credentials"), 65 | (Some(scheme), Some(credentials)) => (scheme.parse()?, credentials.to_owned()), 66 | }; 67 | 68 | Ok(Some(Self { 69 | scheme, 70 | credentials, 71 | })) 72 | } 73 | 74 | /// Get the authorization scheme. 75 | pub fn scheme(&self) -> AuthenticationScheme { 76 | self.scheme 77 | } 78 | 79 | /// Set the authorization scheme. 80 | pub fn set_scheme(&mut self, scheme: AuthenticationScheme) { 81 | self.scheme = scheme; 82 | } 83 | 84 | /// Get the authorization credentials. 85 | pub fn credentials(&self) -> &str { 86 | self.credentials.as_str() 87 | } 88 | 89 | /// Set the authorization credentials. 90 | pub fn set_credentials(&mut self, credentials: String) { 91 | self.credentials = credentials; 92 | } 93 | } 94 | 95 | impl Header for Authorization { 96 | fn header_name(&self) -> HeaderName { 97 | AUTHORIZATION 98 | } 99 | 100 | fn header_value(&self) -> HeaderValue { 101 | let output = format!("{} {}", self.scheme, self.credentials); 102 | 103 | // SAFETY: the internal string is validated to be ASCII. 104 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod test { 110 | use super::*; 111 | use crate::headers::Headers; 112 | 113 | #[test] 114 | fn smoke() -> crate::Result<()> { 115 | let scheme = AuthenticationScheme::Basic; 116 | let credentials = "0xdeadbeef202020"; 117 | let authz = Authorization::new(scheme, credentials.into()); 118 | 119 | let mut headers = Headers::new(); 120 | authz.apply_header(&mut headers); 121 | 122 | let authz = Authorization::from_headers(headers)?.unwrap(); 123 | 124 | assert_eq!(authz.scheme(), AuthenticationScheme::Basic); 125 | assert_eq!(authz.credentials(), credentials); 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn bad_request_on_parse_error() { 131 | let mut headers = Headers::new(); 132 | headers 133 | .insert(AUTHORIZATION, "") 134 | .unwrap(); 135 | let err = Authorization::from_headers(headers).unwrap_err(); 136 | assert_eq!(err.status(), 400); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/auth/basic_auth.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{HeaderName, HeaderValue, Headers, AUTHORIZATION}; 2 | use crate::Status; 3 | use crate::{ 4 | auth::{AuthenticationScheme, Authorization}, 5 | headers::Header, 6 | }; 7 | use crate::{bail_status as bail, ensure_status as ensure}; 8 | 9 | /// HTTP Basic authorization. 10 | /// 11 | /// # Specifications 12 | /// 13 | /// - [RFC7617](https://tools.ietf.org/html/rfc7617) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::Response; 21 | /// use http_types::auth::{AuthenticationScheme, BasicAuth}; 22 | /// 23 | /// let username = "nori"; 24 | /// let password = "secret_fish!!"; 25 | /// let authz = BasicAuth::new(username, password); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&authz, &authz); 29 | /// 30 | /// let authz = BasicAuth::from_headers(res)?.unwrap(); 31 | /// 32 | /// assert_eq!(authz.username(), username); 33 | /// assert_eq!(authz.password(), password); 34 | /// # 35 | /// # Ok(()) } 36 | /// ``` 37 | #[derive(Debug)] 38 | pub struct BasicAuth { 39 | username: String, 40 | password: String, 41 | } 42 | 43 | impl BasicAuth { 44 | /// Create a new instance of `BasicAuth`. 45 | pub fn new(username: U, password: P) -> Self 46 | where 47 | U: AsRef, 48 | P: AsRef, 49 | { 50 | let username = username.as_ref().to_owned(); 51 | let password = password.as_ref().to_owned(); 52 | Self { username, password } 53 | } 54 | 55 | /// Create a new instance from headers. 56 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 57 | let auth = match Authorization::from_headers(headers)? { 58 | Some(auth) => auth, 59 | None => return Ok(None), 60 | }; 61 | 62 | let scheme = auth.scheme(); 63 | ensure!( 64 | matches!(scheme, AuthenticationScheme::Basic), 65 | 400, 66 | "Expected basic auth scheme found `{}`", 67 | scheme 68 | ); 69 | Self::from_credentials(auth.credentials()).map(Some) 70 | } 71 | 72 | /// Create a new instance from the base64 encoded credentials. 73 | pub fn from_credentials(credentials: impl AsRef<[u8]>) -> crate::Result { 74 | let bytes = base64::decode(credentials).status(400)?; 75 | let credentials = String::from_utf8(bytes).status(400)?; 76 | 77 | let mut iter = credentials.splitn(2, ':'); 78 | let username = iter.next(); 79 | let password = iter.next(); 80 | 81 | let (username, password) = match (username, password) { 82 | (Some(username), Some(password)) => (username.to_string(), password.to_string()), 83 | (Some(_), None) => bail!(400, "Expected basic auth to contain a password"), 84 | (None, _) => bail!(400, "Expected basic auth to contain a username"), 85 | }; 86 | 87 | Ok(Self { username, password }) 88 | } 89 | 90 | /// Get the username. 91 | pub fn username(&self) -> &str { 92 | self.username.as_str() 93 | } 94 | 95 | /// Get the password. 96 | pub fn password(&self) -> &str { 97 | self.password.as_str() 98 | } 99 | } 100 | 101 | impl Header for BasicAuth { 102 | fn header_name(&self) -> HeaderName { 103 | AUTHORIZATION 104 | } 105 | 106 | fn header_value(&self) -> HeaderValue { 107 | let scheme = AuthenticationScheme::Basic; 108 | let credentials = base64::encode(format!("{}:{}", self.username, self.password)); 109 | let auth = Authorization::new(scheme, credentials); 110 | auth.header_value() 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod test { 116 | use super::*; 117 | use crate::headers::Headers; 118 | 119 | #[test] 120 | fn smoke() -> crate::Result<()> { 121 | let username = "nori"; 122 | let password = "secret_fish!!"; 123 | let authz = BasicAuth::new(username, password); 124 | 125 | let mut headers = Headers::new(); 126 | authz.apply_header(&mut headers); 127 | 128 | let authz = BasicAuth::from_headers(headers)?.unwrap(); 129 | 130 | assert_eq!(authz.username(), username); 131 | assert_eq!(authz.password(), password); 132 | Ok(()) 133 | } 134 | 135 | #[test] 136 | fn bad_request_on_parse_error() { 137 | let mut headers = Headers::new(); 138 | headers 139 | .insert(AUTHORIZATION, "") 140 | .unwrap(); 141 | let err = BasicAuth::from_headers(headers).unwrap_err(); 142 | assert_eq!(err.status(), 400); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP authentication and authorization. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ``` 6 | //! # fn main() -> http_types::Result<()> { 7 | //! # 8 | //! use http_types::Response; 9 | //! use http_types::auth::{AuthenticationScheme, BasicAuth}; 10 | //! 11 | //! let username = "nori"; 12 | //! let password = "secret_fish!!"; 13 | //! let authz = BasicAuth::new(username, password); 14 | //! 15 | //! let mut res = Response::new(200); 16 | //! res.insert_header(&authz, &authz); 17 | //! 18 | //! let authz = BasicAuth::from_headers(res)?.unwrap(); 19 | //! 20 | //! assert_eq!(authz.username(), username); 21 | //! assert_eq!(authz.password(), password); 22 | //! # 23 | //! # Ok(()) } 24 | //! ``` 25 | 26 | mod authentication_scheme; 27 | mod authorization; 28 | mod basic_auth; 29 | mod www_authenticate; 30 | 31 | pub use authentication_scheme::AuthenticationScheme; 32 | pub use authorization::Authorization; 33 | pub use basic_auth::BasicAuth; 34 | pub use www_authenticate::WwwAuthenticate; 35 | -------------------------------------------------------------------------------- /src/auth/www_authenticate.rs: -------------------------------------------------------------------------------- 1 | use crate::bail_status as bail; 2 | use crate::headers::{HeaderName, HeaderValue, Headers, WWW_AUTHENTICATE}; 3 | use crate::{auth::AuthenticationScheme, headers::Header}; 4 | 5 | /// Define the authentication method that should be used to gain access to a 6 | /// resource. 7 | /// 8 | /// # Specifications 9 | /// 10 | /// - [RFC 7235, section 4.1: WWW-Authenticate](https://tools.ietf.org/html/rfc7235#section-4.1) 11 | /// 12 | /// # Implementation Notes 13 | /// 14 | /// This implementation only encodes and parses a single authentication method, 15 | /// further authorization methods are ignored. It also always passes the utf-8 encoding flag. 16 | /// 17 | /// # Examples 18 | /// 19 | /// ``` 20 | /// # fn main() -> http_types::Result<()> { 21 | /// # 22 | /// use http_types::Response; 23 | /// use http_types::auth::{AuthenticationScheme, WwwAuthenticate}; 24 | /// 25 | /// let scheme = AuthenticationScheme::Basic; 26 | /// let realm = "Access to the staging site"; 27 | /// let authz = WwwAuthenticate::new(scheme, realm.into()); 28 | /// 29 | /// let mut res = Response::new(200); 30 | /// res.insert_header(&authz, &authz); 31 | /// 32 | /// let authz = WwwAuthenticate::from_headers(res)?.unwrap(); 33 | /// 34 | /// assert_eq!(authz.scheme(), AuthenticationScheme::Basic); 35 | /// assert_eq!(authz.realm(), realm); 36 | /// # 37 | /// # Ok(()) } 38 | /// ``` 39 | #[derive(Debug)] 40 | pub struct WwwAuthenticate { 41 | scheme: AuthenticationScheme, 42 | realm: String, 43 | } 44 | 45 | impl WwwAuthenticate { 46 | /// Create a new instance of `WwwAuthenticate`. 47 | pub fn new(scheme: AuthenticationScheme, realm: String) -> Self { 48 | Self { scheme, realm } 49 | } 50 | 51 | /// Create a new instance from headers. 52 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 53 | let headers = match headers.as_ref().get(WWW_AUTHENTICATE) { 54 | Some(headers) => headers, 55 | None => return Ok(None), 56 | }; 57 | 58 | // If we successfully parsed the header then there's always at least one 59 | // entry. We want the last entry. 60 | let value = headers.iter().last().unwrap(); 61 | 62 | let mut iter = value.as_str().splitn(2, ' '); 63 | let scheme = iter.next(); 64 | let credential = iter.next(); 65 | let (scheme, realm) = match (scheme, credential) { 66 | (None, _) => bail!(400, "Could not find scheme"), 67 | (Some(_), None) => bail!(400, "Could not find realm"), 68 | (Some(scheme), Some(realm)) => (scheme.parse()?, realm.to_owned()), 69 | }; 70 | 71 | let realm = realm.trim_start(); 72 | let realm = match realm.strip_prefix(r#"realm=""#) { 73 | Some(realm) => realm, 74 | None => bail!(400, "realm not found"), 75 | }; 76 | 77 | let mut chars = realm.chars(); 78 | let mut closing_quote = false; 79 | let realm = (&mut chars) 80 | .take_while(|c| { 81 | if c == &'"' { 82 | closing_quote = true; 83 | false 84 | } else { 85 | true 86 | } 87 | }) 88 | .collect(); 89 | if !closing_quote { 90 | bail!(400, r"Expected a closing quote"); 91 | } 92 | 93 | Ok(Some(Self { scheme, realm })) 94 | } 95 | 96 | /// Get the authorization scheme. 97 | pub fn scheme(&self) -> AuthenticationScheme { 98 | self.scheme 99 | } 100 | 101 | /// Set the authorization scheme. 102 | pub fn set_scheme(&mut self, scheme: AuthenticationScheme) { 103 | self.scheme = scheme; 104 | } 105 | 106 | /// Get the authorization realm. 107 | pub fn realm(&self) -> &str { 108 | self.realm.as_str() 109 | } 110 | 111 | /// Set the authorization realm. 112 | pub fn set_realm(&mut self, realm: String) { 113 | self.realm = realm; 114 | } 115 | } 116 | 117 | impl Header for WwwAuthenticate { 118 | fn header_name(&self) -> HeaderName { 119 | WWW_AUTHENTICATE 120 | } 121 | 122 | fn header_value(&self) -> HeaderValue { 123 | let output = format!(r#"{} realm="{}", charset="UTF-8""#, self.scheme, self.realm); 124 | 125 | // SAFETY: the internal string is validated to be ASCII. 126 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 127 | } 128 | } 129 | 130 | #[cfg(test)] 131 | mod test { 132 | use super::*; 133 | use crate::headers::Headers; 134 | 135 | #[test] 136 | fn smoke() -> crate::Result<()> { 137 | let scheme = AuthenticationScheme::Basic; 138 | let realm = "Access to the staging site"; 139 | let authz = WwwAuthenticate::new(scheme, realm.into()); 140 | 141 | let mut headers = Headers::new(); 142 | authz.apply_header(&mut headers); 143 | 144 | assert_eq!( 145 | headers["WWW-Authenticate"], 146 | r#"Basic realm="Access to the staging site", charset="UTF-8""# 147 | ); 148 | 149 | let authz = WwwAuthenticate::from_headers(headers)?.unwrap(); 150 | 151 | assert_eq!(authz.scheme(), AuthenticationScheme::Basic); 152 | assert_eq!(authz.realm(), realm); 153 | Ok(()) 154 | } 155 | 156 | #[test] 157 | fn bad_request_on_parse_error() { 158 | let mut headers = Headers::new(); 159 | headers 160 | .insert(WWW_AUTHENTICATE, "") 161 | .unwrap(); 162 | let err = WwwAuthenticate::from_headers(headers).unwrap_err(); 163 | assert_eq!(err.status(), 400); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/cache/age.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, AGE}; 2 | use crate::Status; 3 | 4 | use std::fmt::Debug; 5 | 6 | use std::time::Duration; 7 | 8 | /// HTTP `Age` header 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7234, section 5.1: Age](https://tools.ietf.org/html/rfc7234#section-5.1) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::Response; 20 | /// use http_types::cache::Age; 21 | /// 22 | /// let age = Age::from_secs(12); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&age, &age); 26 | /// 27 | /// let age = Age::from_headers(res)?.unwrap(); 28 | /// assert_eq!(age, Age::from_secs(12)); 29 | /// # 30 | /// # Ok(()) } 31 | /// ``` 32 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 33 | pub struct Age { 34 | dur: Duration, 35 | } 36 | 37 | impl Age { 38 | /// Create a new instance of `Age`. 39 | pub fn new(dur: Duration) -> Self { 40 | Self { dur } 41 | } 42 | 43 | /// Create a new instance of `Age` from secs. 44 | pub fn from_secs(secs: u64) -> Self { 45 | let dur = Duration::from_secs(secs); 46 | Self { dur } 47 | } 48 | 49 | /// Get the duration from the header. 50 | pub fn duration(&self) -> Duration { 51 | self.dur 52 | } 53 | 54 | /// Create an instance of `Age` from a `Headers` instance. 55 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 56 | let headers = match headers.as_ref().get(AGE) { 57 | Some(headers) => headers, 58 | None => return Ok(None), 59 | }; 60 | 61 | // If we successfully parsed the header then there's always at least one 62 | // entry. We want the last entry. 63 | let header = headers.iter().last().unwrap(); 64 | 65 | let num: u64 = header.as_str().parse::().status(400)?; 66 | let dur = Duration::from_secs_f64(num as f64); 67 | 68 | Ok(Some(Self { dur })) 69 | } 70 | } 71 | 72 | impl Header for Age { 73 | fn header_name(&self) -> HeaderName { 74 | AGE 75 | } 76 | 77 | fn header_value(&self) -> HeaderValue { 78 | let output = self.dur.as_secs().to_string(); 79 | 80 | // SAFETY: the internal string is validated to be ASCII. 81 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod test { 87 | use super::*; 88 | use crate::headers::Headers; 89 | 90 | #[test] 91 | fn smoke() -> crate::Result<()> { 92 | let age = Age::new(Duration::from_secs(12)); 93 | 94 | let mut headers = Headers::new(); 95 | age.apply_header(&mut headers); 96 | 97 | let age = Age::from_headers(headers)?.unwrap(); 98 | assert_eq!(age, Age::new(Duration::from_secs(12))); 99 | Ok(()) 100 | } 101 | 102 | #[test] 103 | fn bad_request_on_parse_error() { 104 | let mut headers = Headers::new(); 105 | headers.insert(AGE, "").unwrap(); 106 | let err = Age::from_headers(headers).unwrap_err(); 107 | assert_eq!(err.status(), 400); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/cache/cache_control/cache_control.rs: -------------------------------------------------------------------------------- 1 | use headers::Header; 2 | 3 | use crate::headers::{HeaderName, HeaderValue, Headers, CACHE_CONTROL}; 4 | use crate::{cache::CacheDirective, headers}; 5 | 6 | use std::fmt::{self, Debug, Write}; 7 | use std::iter::Iterator; 8 | 9 | use std::slice; 10 | 11 | /// A Cache-Control header. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// # fn main() -> http_types::Result<()> { 17 | /// # 18 | /// use http_types::Response; 19 | /// use http_types::cache::{CacheControl, CacheDirective}; 20 | /// let mut entries = CacheControl::new(); 21 | /// entries.push(CacheDirective::Immutable); 22 | /// entries.push(CacheDirective::NoStore); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&entries, &entries); 26 | /// 27 | /// let entries = CacheControl::from_headers(res)?.unwrap(); 28 | /// let mut entries = entries.iter(); 29 | /// assert_eq!(entries.next().unwrap(), &CacheDirective::Immutable); 30 | /// assert_eq!(entries.next().unwrap(), &CacheDirective::NoStore); 31 | /// # 32 | /// # Ok(()) } 33 | /// ``` 34 | pub struct CacheControl { 35 | entries: Vec, 36 | } 37 | 38 | impl CacheControl { 39 | /// Create a new instance of `CacheControl`. 40 | pub fn new() -> Self { 41 | Self { entries: vec![] } 42 | } 43 | 44 | /// Create a new instance from headers. 45 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 46 | let mut entries = vec![]; 47 | let headers = match headers.as_ref().get(CACHE_CONTROL) { 48 | Some(headers) => headers, 49 | None => return Ok(None), 50 | }; 51 | 52 | for value in headers { 53 | for part in value.as_str().trim().split(',') { 54 | // Try and parse a directive from a str. If the directive is 55 | // unkown we skip it. 56 | if let Some(entry) = CacheDirective::from_str(part)? { 57 | entries.push(entry); 58 | } 59 | } 60 | } 61 | 62 | Ok(Some(Self { entries })) 63 | } 64 | /// Push a directive into the list of entries. 65 | pub fn push(&mut self, directive: CacheDirective) { 66 | self.entries.push(directive); 67 | } 68 | 69 | /// An iterator visiting all server entries. 70 | pub fn iter(&self) -> Iter<'_> { 71 | Iter { 72 | inner: self.entries.iter(), 73 | } 74 | } 75 | 76 | /// An iterator visiting all server entries. 77 | pub fn iter_mut(&mut self) -> IterMut<'_> { 78 | IterMut { 79 | inner: self.entries.iter_mut(), 80 | } 81 | } 82 | } 83 | 84 | impl Header for CacheControl { 85 | fn header_name(&self) -> HeaderName { 86 | CACHE_CONTROL 87 | } 88 | fn header_value(&self) -> HeaderValue { 89 | let mut output = String::new(); 90 | for (n, directive) in self.entries.iter().enumerate() { 91 | let directive: HeaderValue = directive.clone().into(); 92 | match n { 93 | 0 => write!(output, "{}", directive).unwrap(), 94 | _ => write!(output, ", {}", directive).unwrap(), 95 | }; 96 | } 97 | 98 | // SAFETY: the internal string is validated to be ASCII. 99 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 100 | } 101 | } 102 | 103 | impl IntoIterator for CacheControl { 104 | type Item = CacheDirective; 105 | type IntoIter = IntoIter; 106 | 107 | #[inline] 108 | fn into_iter(self) -> Self::IntoIter { 109 | IntoIter { 110 | inner: self.entries.into_iter(), 111 | } 112 | } 113 | } 114 | 115 | impl<'a> IntoIterator for &'a CacheControl { 116 | type Item = &'a CacheDirective; 117 | type IntoIter = Iter<'a>; 118 | 119 | #[inline] 120 | fn into_iter(self) -> Self::IntoIter { 121 | self.iter() 122 | } 123 | } 124 | 125 | impl<'a> IntoIterator for &'a mut CacheControl { 126 | type Item = &'a mut CacheDirective; 127 | type IntoIter = IterMut<'a>; 128 | 129 | #[inline] 130 | fn into_iter(self) -> Self::IntoIter { 131 | self.iter_mut() 132 | } 133 | } 134 | 135 | /// A borrowing iterator over entries in `CacheControl`. 136 | #[derive(Debug)] 137 | pub struct IntoIter { 138 | inner: std::vec::IntoIter, 139 | } 140 | 141 | impl Iterator for IntoIter { 142 | type Item = CacheDirective; 143 | 144 | fn next(&mut self) -> Option { 145 | self.inner.next() 146 | } 147 | 148 | #[inline] 149 | fn size_hint(&self) -> (usize, Option) { 150 | self.inner.size_hint() 151 | } 152 | } 153 | 154 | /// A lending iterator over entries in `CacheControl`. 155 | #[derive(Debug)] 156 | pub struct Iter<'a> { 157 | inner: slice::Iter<'a, CacheDirective>, 158 | } 159 | 160 | impl<'a> Iterator for Iter<'a> { 161 | type Item = &'a CacheDirective; 162 | 163 | fn next(&mut self) -> Option { 164 | self.inner.next() 165 | } 166 | 167 | #[inline] 168 | fn size_hint(&self) -> (usize, Option) { 169 | self.inner.size_hint() 170 | } 171 | } 172 | 173 | /// A mutable iterator over entries in `CacheControl`. 174 | #[derive(Debug)] 175 | pub struct IterMut<'a> { 176 | inner: slice::IterMut<'a, CacheDirective>, 177 | } 178 | 179 | impl<'a> Iterator for IterMut<'a> { 180 | type Item = &'a mut CacheDirective; 181 | 182 | fn next(&mut self) -> Option { 183 | self.inner.next() 184 | } 185 | 186 | #[inline] 187 | fn size_hint(&self) -> (usize, Option) { 188 | self.inner.size_hint() 189 | } 190 | } 191 | 192 | impl Debug for CacheControl { 193 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 194 | let mut list = f.debug_list(); 195 | for directive in &self.entries { 196 | list.entry(directive); 197 | } 198 | list.finish() 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/cache/cache_control/cache_directive.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::HeaderValue; 2 | use crate::Status; 3 | 4 | use std::time::Duration; 5 | 6 | /// An HTTP `Cache-Control` directive. 7 | #[non_exhaustive] 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub enum CacheDirective { 10 | /// The response body will not change over time. 11 | Immutable, 12 | /// The maximum amount of time a resource is considered fresh. 13 | MaxAge(Duration), 14 | /// Indicates the client will accept a stale response. 15 | MaxStale(Option), 16 | /// A response that will still be fresh for at least the specified duration. 17 | MinFresh(Duration), 18 | /// Once a response is stale, a fresh response must be retrieved. 19 | MustRevalidate, 20 | /// The response may be cached, but must always be revalidated before being used. 21 | NoCache, 22 | /// The response may not be cached. 23 | NoStore, 24 | /// An intermediate cache or proxy should not edit the response body, 25 | /// Content-Encoding, Content-Range, or Content-Type. 26 | NoTransform, 27 | /// Do not use the network for a response. 28 | OnlyIfCached, 29 | /// The response may be stored only by a browser's cache, even if the 30 | /// response is normally non-cacheable. 31 | Private, 32 | /// Like must-revalidate, but only for shared caches (e.g., proxies). 33 | ProxyRevalidate, 34 | /// The response may be stored by any cache, even if the response is normally 35 | /// non-cacheable. 36 | Public, 37 | /// Overrides max-age or the Expires header, but only for shared caches. 38 | SMaxAge(Duration), 39 | /// The client will accept a stale response if retrieving a fresh one fails. 40 | StaleIfError(Duration), 41 | /// Indicates the client will accept a stale response, while asynchronously 42 | /// checking in the background for a fresh one. 43 | StaleWhileRevalidate(Duration), 44 | } 45 | 46 | impl CacheDirective { 47 | /// Check whether this directive is valid in an HTTP request. 48 | pub fn valid_in_req(&self) -> bool { 49 | use CacheDirective::*; 50 | matches!( 51 | self, 52 | MaxAge(_) | MaxStale(_) | MinFresh(_) | NoCache | NoStore | NoTransform | OnlyIfCached 53 | ) 54 | } 55 | 56 | /// Check whether this directive is valid in an HTTP response. 57 | pub fn valid_in_res(&self) -> bool { 58 | use CacheDirective::*; 59 | matches!( 60 | self, 61 | MustRevalidate 62 | | NoCache 63 | | NoStore 64 | | NoTransform 65 | | Public 66 | | Private 67 | | ProxyRevalidate 68 | | MaxAge(_) 69 | | SMaxAge(_) 70 | | StaleIfError(_) 71 | | StaleWhileRevalidate(_) 72 | ) 73 | } 74 | 75 | /// Create an instance from a string slice. 76 | // 77 | // This is a private method rather than a trait because we assume the 78 | // input string is a single-value only. This is upheld by the calling 79 | // function, but we cannot guarantee this to be true in the general 80 | // sense. 81 | pub(crate) fn from_str(s: &str) -> crate::Result> { 82 | use CacheDirective::*; 83 | 84 | let s = s.trim(); 85 | 86 | // We're dealing with an empty string. 87 | if s.is_empty() { 88 | return Ok(None); 89 | } 90 | 91 | let s = s.to_lowercase(); 92 | let mut parts = s.split('='); 93 | let next = parts.next().unwrap(); 94 | 95 | let mut get_dur = || -> crate::Result { 96 | let dur = parts.next().status(400)?; 97 | let dur: u64 = dur.parse::().status(400)?; 98 | Ok(Duration::new(dur, 0)) 99 | }; 100 | 101 | // This won't panic because each input string has at least one part. 102 | let res = match next { 103 | "immutable" => Some(Immutable), 104 | "no-cache" => Some(NoCache), 105 | "no-store" => Some(NoStore), 106 | "no-transform" => Some(NoTransform), 107 | "only-if-cached" => Some(OnlyIfCached), 108 | "must-revalidate" => Some(MustRevalidate), 109 | "public" => Some(Public), 110 | "private" => Some(Private), 111 | "proxy-revalidate" => Some(ProxyRevalidate), 112 | "max-age" => Some(MaxAge(get_dur()?)), 113 | "max-stale" => match parts.next() { 114 | Some(secs) => { 115 | let dur: u64 = secs.parse::().status(400)?; 116 | Some(MaxStale(Some(Duration::new(dur, 0)))) 117 | } 118 | None => Some(MaxStale(None)), 119 | }, 120 | "min-fresh" => Some(MinFresh(get_dur()?)), 121 | "s-maxage" => Some(SMaxAge(get_dur()?)), 122 | "stale-if-error" => Some(StaleIfError(get_dur()?)), 123 | "stale-while-revalidate" => Some(StaleWhileRevalidate(get_dur()?)), 124 | _ => None, 125 | }; 126 | Ok(res) 127 | } 128 | } 129 | 130 | impl From for HeaderValue { 131 | fn from(directive: CacheDirective) -> Self { 132 | use CacheDirective::*; 133 | let h = |s: String| unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) }; 134 | 135 | match directive { 136 | Immutable => h("immutable".to_string()), 137 | MaxAge(dur) => h(format!("max-age={}", dur.as_secs())), 138 | MaxStale(dur) => match dur { 139 | Some(dur) => h(format!("max-stale={}", dur.as_secs())), 140 | None => h("max-stale".to_string()), 141 | }, 142 | MinFresh(dur) => h(format!("min-fresh={}", dur.as_secs())), 143 | MustRevalidate => h("must-revalidate".to_string()), 144 | NoCache => h("no-cache".to_string()), 145 | NoStore => h("no-store".to_string()), 146 | NoTransform => h("no-transform".to_string()), 147 | OnlyIfCached => h("only-if-cached".to_string()), 148 | Private => h("private".to_string()), 149 | ProxyRevalidate => h("proxy-revalidate".to_string()), 150 | Public => h("public".to_string()), 151 | SMaxAge(dur) => h(format!("s-max-age={}", dur.as_secs())), 152 | StaleIfError(dur) => h(format!("stale-if-error={}", dur.as_secs())), 153 | StaleWhileRevalidate(dur) => h(format!("stale-while-revalidate={}", dur.as_secs())), 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/cache/cache_control/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP `Cache-Control` headers. 2 | //! 3 | //! # Specifications 4 | //! 5 | //! - [RFC 8246: HTTP Immutable Responses](https://tools.ietf.org/html/rfc8246) 6 | //! - [RFC 7234: Hypertext Transfer Protocol (HTTP/1.1): Caching](https://tools.ietf.org/html/rfc7234) 7 | //! - [RFC 5861: HTTP Cache-Control Extensions for Stale Content](https://tools.ietf.org/html/rfc5861) 8 | 9 | #[allow(clippy::module_inception)] 10 | mod cache_control; 11 | mod cache_directive; 12 | 13 | pub use cache_control::CacheControl; 14 | pub use cache_directive::CacheDirective; 15 | 16 | #[cfg(test)] 17 | mod test { 18 | use super::*; 19 | use crate::headers::{Header, Headers, CACHE_CONTROL}; 20 | 21 | #[test] 22 | fn smoke() -> crate::Result<()> { 23 | let mut entries = CacheControl::new(); 24 | entries.push(CacheDirective::Immutable); 25 | entries.push(CacheDirective::NoStore); 26 | 27 | let mut headers = Headers::new(); 28 | entries.apply_header(&mut headers); 29 | 30 | let entries = CacheControl::from_headers(headers)?.unwrap(); 31 | let mut entries = entries.iter(); 32 | assert_eq!(entries.next().unwrap(), &CacheDirective::Immutable); 33 | assert_eq!(entries.next().unwrap(), &CacheDirective::NoStore); 34 | Ok(()) 35 | } 36 | 37 | #[test] 38 | fn ignore_unkonwn_directives() -> crate::Result<()> { 39 | let mut headers = Headers::new(); 40 | headers.insert(CACHE_CONTROL, "barrel_roll").unwrap(); 41 | let entries = CacheControl::from_headers(headers)?.unwrap(); 42 | let mut entries = entries.iter(); 43 | assert!(entries.next().is_none()); 44 | Ok(()) 45 | } 46 | 47 | #[test] 48 | fn bad_request_on_parse_error() { 49 | let mut headers = Headers::new(); 50 | headers.insert(CACHE_CONTROL, "min-fresh=0.9").unwrap(); // floats are not supported 51 | let err = CacheControl::from_headers(headers).unwrap_err(); 52 | assert_eq!(err.status(), 400); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cache/clear_site_data/directive.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::str::FromStr; 3 | 4 | /// An HTTP `Clear-Site-Data` directive. 5 | /// 6 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data#Directives) 7 | #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] 8 | pub enum ClearDirective { 9 | /// Indicates that the server wishes to remove locally cached data (i.e. the 10 | /// browser cache, see HTTP caching) for the origin of the response URL. 11 | /// Depending on the browser, this might also clear out things like 12 | /// pre-rendered pages, script caches, WebGL shader caches, or address bar 13 | /// suggestions. 14 | Cache, 15 | /// Indicates that the server wishes to remove all cookies for the origin of 16 | /// the response URL. HTTP authentication credentials are also cleared out. 17 | /// This affects the entire registered domain, including subdomains. So 18 | /// https://example.com as well as https://stage.example.com, will have 19 | /// cookies cleared. 20 | Cookies, 21 | /// Indicates that the server wishes to remove all DOM storage for the origin 22 | /// of the response URL. 23 | Storage, 24 | /// Indicates that the server wishes to reload all browsing contexts for the 25 | /// origin of the response (Location.reload). 26 | ExecutionContexts, 27 | } 28 | 29 | impl ClearDirective { 30 | /// Get the formatted string. 31 | pub fn as_str(&self) -> &'static str { 32 | match self { 33 | ClearDirective::Cache => r#""cache""#, 34 | ClearDirective::Cookies => r#""cookies""#, 35 | ClearDirective::Storage => r#""storage""#, 36 | ClearDirective::ExecutionContexts => r#""executionContexts""#, 37 | } 38 | } 39 | } 40 | 41 | impl Display for ClearDirective { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!(f, "{}", self.as_str()) 44 | } 45 | } 46 | 47 | impl FromStr for ClearDirective { 48 | type Err = std::string::ParseError; 49 | 50 | fn from_str(s: &str) -> Result { 51 | match s { 52 | r#""cache""# => Ok(Self::Cache), 53 | r#""cookies""# => Ok(Self::Cookies), 54 | r#""storage""# => Ok(Self::Storage), 55 | r#""executionContexts""# => Ok(Self::ExecutionContexts), 56 | _ => todo!(), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/cache/expires.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, EXPIRES}; 2 | use crate::utils::{fmt_http_date, parse_http_date}; 3 | 4 | use std::fmt::Debug; 5 | use std::time::{Duration, SystemTime}; 6 | 7 | /// HTTP `Expires` header 8 | /// 9 | /// # Specifications 10 | /// 11 | /// - [RFC 7234, section 5.3: Expires](https://tools.ietf.org/html/rfc7234#section-5.3) 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// # fn main() -> http_types::Result<()> { 17 | /// # 18 | /// use http_types::Response; 19 | /// use http_types::cache::Expires; 20 | /// use std::time::{SystemTime, Duration}; 21 | /// 22 | /// let time = SystemTime::now() + Duration::from_secs(5 * 60); 23 | /// let expires = Expires::new_at(time); 24 | /// 25 | /// let mut res = Response::new(200); 26 | /// res.insert_header(&expires, &expires); 27 | /// 28 | /// let expires = Expires::from_headers(res)?.unwrap(); 29 | /// 30 | /// // HTTP dates only have second-precision. 31 | /// let elapsed = time.duration_since(expires.expiration())?; 32 | /// assert_eq!(elapsed.as_secs(), 0); 33 | /// # 34 | /// # Ok(()) } 35 | /// ``` 36 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 37 | pub struct Expires { 38 | instant: SystemTime, 39 | } 40 | 41 | impl Expires { 42 | /// Create a new instance of `Expires`. 43 | pub fn new(dur: Duration) -> Self { 44 | let instant = SystemTime::now() + dur; 45 | Self { instant } 46 | } 47 | 48 | /// Create a new instance of `Expires` from secs. 49 | pub fn new_at(instant: SystemTime) -> Self { 50 | Self { instant } 51 | } 52 | 53 | /// Get the expiration time. 54 | pub fn expiration(&self) -> SystemTime { 55 | self.instant 56 | } 57 | 58 | /// Create an instance of `Expires` from a `Headers` instance. 59 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 60 | let headers = match headers.as_ref().get(EXPIRES) { 61 | Some(headers) => headers, 62 | None => return Ok(None), 63 | }; 64 | 65 | // If we successfully parsed the header then there's always at least one 66 | // entry. We want the last entry. 67 | let header = headers.iter().last().unwrap(); 68 | 69 | let instant = parse_http_date(header.as_str())?; 70 | Ok(Some(Self { instant })) 71 | } 72 | } 73 | 74 | impl Header for Expires { 75 | fn header_name(&self) -> HeaderName { 76 | EXPIRES 77 | } 78 | fn header_value(&self) -> HeaderValue { 79 | let output = fmt_http_date(self.instant); 80 | 81 | // SAFETY: the internal string is validated to be ASCII. 82 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod test { 88 | use super::*; 89 | use crate::headers::Headers; 90 | 91 | #[test] 92 | fn smoke() -> crate::Result<()> { 93 | let time = SystemTime::now() + Duration::from_secs(5 * 60); 94 | let expires = Expires::new_at(time); 95 | 96 | let mut headers = Headers::new(); 97 | expires.apply_header(&mut headers); 98 | 99 | let expires = Expires::from_headers(headers)?.unwrap(); 100 | 101 | // HTTP dates only have second-precision 102 | let elapsed = time.duration_since(expires.expiration())?; 103 | assert_eq!(elapsed.as_secs(), 0); 104 | Ok(()) 105 | } 106 | 107 | #[test] 108 | fn bad_request_on_parse_error() { 109 | let mut headers = Headers::new(); 110 | headers.insert(EXPIRES, "").unwrap(); 111 | let err = Expires::from_headers(headers).unwrap_err(); 112 | assert_eq!(err.status(), 400); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP caching. 2 | //! 3 | //! Web page performance can be significantly improved by caching resources. 4 | //! This submodule includes headers and types to communicate how and when to 5 | //! cache resources. 6 | //! 7 | //! # Further Reading 8 | //! 9 | //! - [MDN: HTTP Caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) 10 | 11 | mod age; 12 | mod cache_control; 13 | mod clear_site_data; 14 | mod expires; 15 | 16 | pub use age::Age; 17 | pub use cache_control::CacheControl; 18 | pub use cache_control::CacheDirective; 19 | pub use clear_site_data::{ClearDirective, ClearSiteData}; 20 | pub use expires::Expires; 21 | -------------------------------------------------------------------------------- /src/conditional/etag.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, ETAG}; 2 | use crate::{Error, StatusCode}; 3 | 4 | use std::fmt::{self, Debug, Display}; 5 | 6 | /// HTTP Entity Tags. 7 | /// 8 | /// ETags provide an ID for a particular resource, enabling clients and servers 9 | /// to reason about caches and make conditional requests. 10 | /// 11 | /// # Specifications 12 | /// 13 | /// - [RFC 7232, section 2.3: ETag](https://tools.ietf.org/html/rfc7232#section-2.3) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::Response; 21 | /// use http_types::conditional::ETag; 22 | /// 23 | /// let etag = ETag::new("0xcafebeef".to_string()); 24 | /// 25 | /// let mut res = Response::new(200); 26 | /// res.insert_header(&etag, &etag); 27 | /// 28 | /// let etag = ETag::from_headers(res)?.unwrap(); 29 | /// assert_eq!(etag, ETag::Strong(String::from("0xcafebeef"))); 30 | /// # 31 | /// # Ok(()) } 32 | /// ``` 33 | #[derive(Debug, Clone, Eq, PartialEq)] 34 | pub enum ETag { 35 | /// An ETag using strong validation. 36 | Strong(String), 37 | /// An ETag using weak validation. 38 | Weak(String), 39 | } 40 | 41 | impl ETag { 42 | /// Create a new ETag that uses strong validation. 43 | pub fn new(s: String) -> Self { 44 | debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars"); 45 | Self::Strong(s) 46 | } 47 | 48 | /// Create a new ETag that uses weak validation. 49 | pub fn new_weak(s: String) -> Self { 50 | debug_assert!(!s.contains('\\'), "ETags ought to avoid backslash chars"); 51 | Self::Weak(s) 52 | } 53 | 54 | /// Create a new instance from headers. 55 | /// 56 | /// Only a single ETag per resource is assumed to exist. If multiple ETag 57 | /// headers are found the last one is used. 58 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 59 | let headers = match headers.as_ref().get(ETAG) { 60 | Some(headers) => headers, 61 | None => return Ok(None), 62 | }; 63 | 64 | // If a header is returned we can assume at least one exists. 65 | let s = headers.iter().last().unwrap().as_str(); 66 | Self::from_str(s).map(Some) 67 | } 68 | 69 | /// Returns `true` if the ETag is a `Strong` value. 70 | pub fn is_strong(&self) -> bool { 71 | matches!(self, Self::Strong(_)) 72 | } 73 | 74 | /// Returns `true` if the ETag is a `Weak` value. 75 | pub fn is_weak(&self) -> bool { 76 | matches!(self, Self::Weak(_)) 77 | } 78 | 79 | /// Create an Etag from a string. 80 | pub(crate) fn from_str(s: &str) -> crate::Result { 81 | let mut weak = false; 82 | let s = match s.strip_prefix("W/") { 83 | Some(s) => { 84 | weak = true; 85 | s 86 | } 87 | None => s, 88 | }; 89 | 90 | let s = match s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) { 91 | Some(s) => s.to_owned(), 92 | None => { 93 | return Err(Error::from_str( 94 | StatusCode::BadRequest, 95 | "Invalid ETag header", 96 | )) 97 | } 98 | }; 99 | 100 | if !s 101 | .bytes() 102 | .all(|c| c == 0x21 || (0x23..=0x7E).contains(&c) || c >= 0x80) 103 | { 104 | return Err(Error::from_str( 105 | StatusCode::BadRequest, 106 | "Invalid ETag header", 107 | )); 108 | } 109 | 110 | let etag = if weak { Self::Weak(s) } else { Self::Strong(s) }; 111 | Ok(etag) 112 | } 113 | } 114 | 115 | impl Header for ETag { 116 | fn header_name(&self) -> HeaderName { 117 | ETAG 118 | } 119 | fn header_value(&self) -> HeaderValue { 120 | let s = self.to_string(); 121 | // SAFETY: the internal string is validated to be ASCII. 122 | unsafe { HeaderValue::from_bytes_unchecked(s.into()) } 123 | } 124 | } 125 | 126 | impl Display for ETag { 127 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 | match self { 129 | Self::Strong(s) => write!(f, r#""{}""#, s), 130 | Self::Weak(s) => write!(f, r#"W/"{}""#, s), 131 | } 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use super::*; 138 | use crate::headers::Headers; 139 | 140 | #[test] 141 | fn smoke() -> crate::Result<()> { 142 | let etag = ETag::new("0xcafebeef".to_string()); 143 | 144 | let mut headers = Headers::new(); 145 | etag.apply_header(&mut headers); 146 | 147 | let etag = ETag::from_headers(headers)?.unwrap(); 148 | assert_eq!(etag, ETag::Strong(String::from("0xcafebeef"))); 149 | Ok(()) 150 | } 151 | 152 | #[test] 153 | fn smoke_weak() -> crate::Result<()> { 154 | let etag = ETag::new_weak("0xcafebeef".to_string()); 155 | 156 | let mut headers = Headers::new(); 157 | etag.apply_header(&mut headers); 158 | 159 | let etag = ETag::from_headers(headers)?.unwrap(); 160 | assert_eq!(etag, ETag::Weak(String::from("0xcafebeef"))); 161 | Ok(()) 162 | } 163 | 164 | #[test] 165 | fn bad_request_on_parse_error() { 166 | let mut headers = Headers::new(); 167 | headers.insert(ETAG, "").unwrap(); 168 | let err = ETag::from_headers(headers).unwrap_err(); 169 | assert_eq!(err.status(), 400); 170 | } 171 | 172 | #[test] 173 | fn validate_quotes() { 174 | assert_entry_err(r#""hello"#, "Invalid ETag header"); 175 | assert_entry_err(r#"hello""#, "Invalid ETag header"); 176 | assert_entry_err(r#"/O"valid content""#, "Invalid ETag header"); 177 | assert_entry_err(r#"/Wvalid content""#, "Invalid ETag header"); 178 | } 179 | 180 | fn assert_entry_err(s: &str, msg: &str) { 181 | let mut headers = Headers::new(); 182 | headers.insert(ETAG, s).unwrap(); 183 | let err = ETag::from_headers(headers).unwrap_err(); 184 | assert_eq!(format!("{}", err), msg); 185 | } 186 | 187 | #[test] 188 | fn validate_characters() { 189 | assert_entry_err(r#"""hello""#, "Invalid ETag header"); 190 | assert_entry_err("\"hello\x7F\"", "Invalid ETag header"); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/conditional/if_modified_since.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, IF_MODIFIED_SINCE}; 2 | use crate::utils::{fmt_http_date, parse_http_date}; 3 | 4 | use std::fmt::Debug; 5 | 6 | use std::time::SystemTime; 7 | 8 | /// Apply the HTTP method if the entity has been modified after the given 9 | /// date. 10 | /// 11 | /// # Specifications 12 | /// 13 | /// - [RFC 7232, section 3.3: If-Modified-Since](https://tools.ietf.org/html/rfc7232#section-3.3) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::Response; 21 | /// use http_types::conditional::IfModifiedSince; 22 | /// use std::time::{SystemTime, Duration}; 23 | /// 24 | /// let time = SystemTime::now() + Duration::from_secs(5 * 60); 25 | /// let expires = IfModifiedSince::new(time); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&expires, &expires); 29 | /// 30 | /// let expires = IfModifiedSince::from_headers(res)?.unwrap(); 31 | /// 32 | /// // HTTP dates only have second-precision. 33 | /// let elapsed = time.duration_since(expires.modified())?; 34 | /// assert_eq!(elapsed.as_secs(), 0); 35 | /// # 36 | /// # Ok(()) } 37 | /// ``` 38 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 39 | pub struct IfModifiedSince { 40 | instant: SystemTime, 41 | } 42 | 43 | impl IfModifiedSince { 44 | /// Create a new instance of `IfModifiedSince`. 45 | pub fn new(instant: SystemTime) -> Self { 46 | Self { instant } 47 | } 48 | 49 | /// Returns the last modification time listed. 50 | pub fn modified(&self) -> SystemTime { 51 | self.instant 52 | } 53 | 54 | /// Create an instance of `IfModifiedSince` from a `Headers` instance. 55 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 56 | let headers = match headers.as_ref().get(IF_MODIFIED_SINCE) { 57 | Some(headers) => headers, 58 | None => return Ok(None), 59 | }; 60 | 61 | // If we successfully parsed the header then there's always at least one 62 | // entry. We want the last entry. 63 | let header = headers.iter().last().unwrap(); 64 | 65 | let instant = parse_http_date(header.as_str())?; 66 | Ok(Some(Self { instant })) 67 | } 68 | } 69 | 70 | impl Header for IfModifiedSince { 71 | fn header_name(&self) -> HeaderName { 72 | IF_MODIFIED_SINCE 73 | } 74 | fn header_value(&self) -> HeaderValue { 75 | let output = fmt_http_date(self.instant); 76 | 77 | // SAFETY: the internal string is validated to be ASCII. 78 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | use super::*; 85 | use crate::headers::Headers; 86 | use std::time::Duration; 87 | 88 | #[test] 89 | fn smoke() -> crate::Result<()> { 90 | let time = SystemTime::now() + Duration::from_secs(5 * 60); 91 | let expires = IfModifiedSince::new(time); 92 | 93 | let mut headers = Headers::new(); 94 | expires.apply_header(&mut headers); 95 | 96 | let expires = IfModifiedSince::from_headers(headers)?.unwrap(); 97 | 98 | // HTTP dates only have second-precision 99 | let elapsed = time.duration_since(expires.modified())?; 100 | assert_eq!(elapsed.as_secs(), 0); 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn bad_request_on_parse_error() { 106 | let mut headers = Headers::new(); 107 | headers 108 | .insert(IF_MODIFIED_SINCE, "") 109 | .unwrap(); 110 | let err = IfModifiedSince::from_headers(headers).unwrap_err(); 111 | assert_eq!(err.status(), 400); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/conditional/if_unmodified_since.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, IF_UNMODIFIED_SINCE}; 2 | use crate::utils::{fmt_http_date, parse_http_date}; 3 | 4 | use std::fmt::Debug; 5 | 6 | use std::time::SystemTime; 7 | 8 | /// Apply the HTTP method if the entity has not been modified after the 9 | /// given date. 10 | /// 11 | /// # Specifications 12 | /// 13 | /// - [RFC 7232, section 3.4: If-Unmodified-Since](https://tools.ietf.org/html/rfc7232#section-3.4) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::Response; 21 | /// use http_types::conditional::IfUnmodifiedSince; 22 | /// use std::time::{SystemTime, Duration}; 23 | /// 24 | /// let time = SystemTime::now() + Duration::from_secs(5 * 60); 25 | /// let expires = IfUnmodifiedSince::new(time); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&expires, &expires); 29 | /// 30 | /// let expires = IfUnmodifiedSince::from_headers(res)?.unwrap(); 31 | /// 32 | /// // HTTP dates only have second-precision. 33 | /// let elapsed = time.duration_since(expires.modified())?; 34 | /// assert_eq!(elapsed.as_secs(), 0); 35 | /// # 36 | /// # Ok(()) } 37 | /// ``` 38 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 39 | pub struct IfUnmodifiedSince { 40 | instant: SystemTime, 41 | } 42 | 43 | impl IfUnmodifiedSince { 44 | /// Create a new instance of `IfUnmodifiedSince`. 45 | pub fn new(instant: SystemTime) -> Self { 46 | Self { instant } 47 | } 48 | 49 | /// Returns the last modification time listed. 50 | pub fn modified(&self) -> SystemTime { 51 | self.instant 52 | } 53 | 54 | /// Create an instance of `IfUnmodifiedSince` from a `Headers` instance. 55 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 56 | let headers = match headers.as_ref().get(IF_UNMODIFIED_SINCE) { 57 | Some(headers) => headers, 58 | None => return Ok(None), 59 | }; 60 | 61 | // If we successfully parsed the header then there's always at least one 62 | // entry. We want the last entry. 63 | let header = headers.iter().last().unwrap(); 64 | 65 | let instant = parse_http_date(header.as_str())?; 66 | Ok(Some(Self { instant })) 67 | } 68 | } 69 | 70 | impl Header for IfUnmodifiedSince { 71 | fn header_name(&self) -> HeaderName { 72 | IF_UNMODIFIED_SINCE 73 | } 74 | fn header_value(&self) -> HeaderValue { 75 | let output = fmt_http_date(self.instant); 76 | 77 | // SAFETY: the internal string is validated to be ASCII. 78 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod test { 84 | use super::*; 85 | use crate::headers::Headers; 86 | use std::time::Duration; 87 | 88 | #[test] 89 | fn smoke() -> crate::Result<()> { 90 | let time = SystemTime::now() + Duration::from_secs(5 * 60); 91 | let expires = IfUnmodifiedSince::new(time); 92 | 93 | let mut headers = Headers::new(); 94 | expires.apply_header(&mut headers); 95 | 96 | let expires = IfUnmodifiedSince::from_headers(headers)?.unwrap(); 97 | 98 | // HTTP dates only have second-precision 99 | let elapsed = time.duration_since(expires.modified())?; 100 | assert_eq!(elapsed.as_secs(), 0); 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn bad_request_on_parse_error() { 106 | let mut headers = Headers::new(); 107 | headers 108 | .insert(IF_UNMODIFIED_SINCE, "") 109 | .unwrap(); 110 | let err = IfUnmodifiedSince::from_headers(headers).unwrap_err(); 111 | assert_eq!(err.status(), 400); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/conditional/last_modified.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, LAST_MODIFIED}; 2 | use crate::utils::{fmt_http_date, parse_http_date}; 3 | 4 | use std::fmt::Debug; 5 | 6 | use std::time::SystemTime; 7 | 8 | /// The last modification date of a resource. 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7232, section 2.2: Last-Modified](https://tools.ietf.org/html/rfc7232#section-2.2) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::Response; 20 | /// use http_types::conditional::LastModified; 21 | /// use std::time::{SystemTime, Duration}; 22 | /// 23 | /// let time = SystemTime::now() + Duration::from_secs(5 * 60); 24 | /// let last_modified = LastModified::new(time); 25 | /// 26 | /// let mut res = Response::new(200); 27 | /// res.insert_header(&last_modified, &last_modified); 28 | /// 29 | /// let last_modified = LastModified::from_headers(res)?.unwrap(); 30 | /// 31 | /// // HTTP dates only have second-precision. 32 | /// let elapsed = time.duration_since(last_modified.modified())?; 33 | /// assert_eq!(elapsed.as_secs(), 0); 34 | /// # 35 | /// # Ok(()) } 36 | /// ``` 37 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 38 | pub struct LastModified { 39 | instant: SystemTime, 40 | } 41 | 42 | impl LastModified { 43 | /// Create a new instance of `LastModified`. 44 | pub fn new(instant: SystemTime) -> Self { 45 | Self { instant } 46 | } 47 | 48 | /// Returns the last modification time listed. 49 | pub fn modified(&self) -> SystemTime { 50 | self.instant 51 | } 52 | 53 | /// Create an instance of `LastModified` from a `Headers` instance. 54 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 55 | let headers = match headers.as_ref().get(LAST_MODIFIED) { 56 | Some(headers) => headers, 57 | None => return Ok(None), 58 | }; 59 | 60 | // If we successfully parsed the header then there's always at least one 61 | // entry. We want the last entry. 62 | let header = headers.iter().last().unwrap(); 63 | 64 | let instant = parse_http_date(header.as_str())?; 65 | Ok(Some(Self { instant })) 66 | } 67 | } 68 | 69 | impl Header for LastModified { 70 | fn header_name(&self) -> HeaderName { 71 | LAST_MODIFIED 72 | } 73 | fn header_value(&self) -> HeaderValue { 74 | let output = fmt_http_date(self.instant); 75 | 76 | // SAFETY: the internal string is validated to be ASCII. 77 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod test { 83 | use super::*; 84 | use crate::headers::Headers; 85 | use std::time::Duration; 86 | 87 | #[test] 88 | fn smoke() -> crate::Result<()> { 89 | let time = SystemTime::now() + Duration::from_secs(5 * 60); 90 | let last_modified = LastModified::new(time); 91 | 92 | let mut headers = Headers::new(); 93 | last_modified.apply_header(&mut headers); 94 | 95 | let last_modified = LastModified::from_headers(headers)?.unwrap(); 96 | 97 | // HTTP dates only have second-precision 98 | let elapsed = time.duration_since(last_modified.modified())?; 99 | assert_eq!(elapsed.as_secs(), 0); 100 | Ok(()) 101 | } 102 | 103 | #[test] 104 | fn bad_request_on_parse_error() { 105 | let mut headers = Headers::new(); 106 | headers 107 | .insert(LAST_MODIFIED, "") 108 | .unwrap(); 109 | let err = LastModified::from_headers(headers).unwrap_err(); 110 | assert_eq!(err.status(), 400); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/conditional/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP conditional headers. 2 | //! 3 | //! Web page performance can be significantly improved by caching resources. 4 | //! This submodule includes headers and types to communicate how and when to 5 | //! cache resources. 6 | //! 7 | //! # Further Reading 8 | //! 9 | //! - [MDN: HTTP Conditional Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) 10 | 11 | mod etag; 12 | mod if_modified_since; 13 | mod if_unmodified_since; 14 | mod last_modified; 15 | mod vary; 16 | 17 | pub mod if_match; 18 | pub mod if_none_match; 19 | 20 | pub use etag::ETag; 21 | pub use vary::Vary; 22 | 23 | #[doc(inline)] 24 | pub use if_match::IfMatch; 25 | #[doc(inline)] 26 | pub use if_modified_since::IfModifiedSince; 27 | #[doc(inline)] 28 | pub use if_none_match::IfNoneMatch; 29 | #[doc(inline)] 30 | pub use if_unmodified_since::IfUnmodifiedSince; 31 | #[doc(inline)] 32 | pub use last_modified::LastModified; 33 | -------------------------------------------------------------------------------- /src/content/content_encoding.rs: -------------------------------------------------------------------------------- 1 | //! Specify the compression algorithm. 2 | 3 | use crate::headers::{HeaderName, HeaderValue, Headers, CONTENT_ENCODING}; 4 | use crate::{ 5 | content::{Encoding, EncodingProposal}, 6 | headers::Header, 7 | }; 8 | 9 | use std::fmt::{self, Debug}; 10 | use std::ops::{Deref, DerefMut}; 11 | 12 | /// Specify the compression algorithm. 13 | /// 14 | /// # Specifications 15 | /// 16 | /// - [RFC 7231, section 3.1.2.2: Content-Encoding](https://tools.ietf.org/html/rfc7231#section-3.1.2.2) 17 | /// 18 | /// # Examples 19 | /// 20 | /// ``` 21 | /// # fn main() -> http_types::Result<()> { 22 | /// # 23 | /// use http_types::Response; 24 | /// use http_types::content::{ContentEncoding, Encoding}; 25 | /// let mut encoding = ContentEncoding::new(Encoding::Gzip); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&encoding, &encoding); 29 | /// 30 | /// let encoding = ContentEncoding::from_headers(res)?.unwrap(); 31 | /// assert_eq!(encoding, &Encoding::Gzip); 32 | /// # 33 | /// # Ok(()) } 34 | /// ``` 35 | pub struct ContentEncoding { 36 | inner: Encoding, 37 | } 38 | 39 | impl ContentEncoding { 40 | /// Create a new instance of `CacheControl`. 41 | pub fn new(encoding: Encoding) -> Self { 42 | Self { inner: encoding } 43 | } 44 | 45 | /// Create a new instance from headers. 46 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 47 | let headers = match headers.as_ref().get(CONTENT_ENCODING) { 48 | Some(headers) => headers, 49 | None => return Ok(None), 50 | }; 51 | 52 | let mut inner = None; 53 | 54 | for value in headers { 55 | if let Some(entry) = Encoding::from_str(value.as_str()) { 56 | inner = Some(entry); 57 | } 58 | } 59 | 60 | let inner = inner.expect("Headers instance with no entries found"); 61 | Ok(Some(Self { inner })) 62 | } 63 | 64 | /// Access the encoding kind. 65 | pub fn encoding(&self) -> Encoding { 66 | self.inner 67 | } 68 | } 69 | 70 | impl Header for ContentEncoding { 71 | fn header_name(&self) -> HeaderName { 72 | CONTENT_ENCODING 73 | } 74 | fn header_value(&self) -> HeaderValue { 75 | self.inner.into() 76 | } 77 | } 78 | 79 | impl Deref for ContentEncoding { 80 | type Target = Encoding; 81 | fn deref(&self) -> &Self::Target { 82 | &self.inner 83 | } 84 | } 85 | 86 | impl DerefMut for ContentEncoding { 87 | fn deref_mut(&mut self) -> &mut Self::Target { 88 | &mut self.inner 89 | } 90 | } 91 | 92 | impl PartialEq for ContentEncoding { 93 | fn eq(&self, other: &Encoding) -> bool { 94 | &self.inner == other 95 | } 96 | } 97 | 98 | impl PartialEq<&Encoding> for ContentEncoding { 99 | fn eq(&self, other: &&Encoding) -> bool { 100 | &&self.inner == other 101 | } 102 | } 103 | 104 | impl From for ContentEncoding { 105 | fn from(encoding: Encoding) -> Self { 106 | Self { inner: encoding } 107 | } 108 | } 109 | 110 | impl From<&Encoding> for ContentEncoding { 111 | fn from(encoding: &Encoding) -> Self { 112 | Self { inner: *encoding } 113 | } 114 | } 115 | 116 | impl From for ContentEncoding { 117 | fn from(encoding: EncodingProposal) -> Self { 118 | Self { 119 | inner: encoding.encoding, 120 | } 121 | } 122 | } 123 | 124 | impl From<&EncodingProposal> for ContentEncoding { 125 | fn from(encoding: &EncodingProposal) -> Self { 126 | Self { 127 | inner: encoding.encoding, 128 | } 129 | } 130 | } 131 | 132 | impl Debug for ContentEncoding { 133 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 134 | self.inner.fmt(f) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/content/content_length.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, CONTENT_LENGTH}; 2 | use crate::Status; 3 | 4 | /// The size of the entity-body, in bytes, sent to the recipient. 5 | /// 6 | /// # Specifications 7 | /// 8 | /// - [RFC 7230, section 3.3.2: Content-Length](https://tools.ietf.org/html/rfc7230#section-3.3.2) 9 | /// 10 | /// # Examples 11 | /// 12 | /// ``` 13 | /// # fn main() -> http_types::Result<()> { 14 | /// # 15 | /// use http_types::Response; 16 | /// use http_types::content::{ContentLength}; 17 | /// 18 | /// let content_len = ContentLength::new(12); 19 | /// 20 | /// let mut res = Response::new(200); 21 | /// res.insert_header(&content_len, &content_len); 22 | /// 23 | /// let content_len = ContentLength::from_headers(res)?.unwrap(); 24 | /// assert_eq!(content_len.len(), 12); 25 | /// # 26 | /// # Ok(()) } 27 | /// ``` 28 | #[derive(Debug)] 29 | pub struct ContentLength { 30 | length: u64, 31 | } 32 | 33 | #[allow(clippy::len_without_is_empty)] 34 | impl ContentLength { 35 | /// Create a new instance. 36 | pub fn new(length: u64) -> Self { 37 | Self { length } 38 | } 39 | 40 | /// Create a new instance from headers. 41 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 42 | let headers = match headers.as_ref().get(CONTENT_LENGTH) { 43 | Some(headers) => headers, 44 | None => return Ok(None), 45 | }; 46 | 47 | // If we successfully parsed the header then there's always at least one 48 | // entry. We want the last entry. 49 | let value = headers.iter().last().unwrap(); 50 | let length = value.as_str().trim().parse::().status(400)?; 51 | Ok(Some(Self { length })) 52 | } 53 | 54 | /// Get the content length. 55 | pub fn len(&self) -> u64 { 56 | self.length 57 | } 58 | 59 | /// Set the content length. 60 | pub fn set_len(&mut self, len: u64) { 61 | self.length = len; 62 | } 63 | } 64 | 65 | impl Header for ContentLength { 66 | fn header_name(&self) -> HeaderName { 67 | CONTENT_LENGTH 68 | } 69 | fn header_value(&self) -> HeaderValue { 70 | let output = format!("{}", self.length); 71 | 72 | // SAFETY: the internal string is validated to be ASCII. 73 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod test { 79 | use super::*; 80 | use crate::headers::Headers; 81 | 82 | #[test] 83 | fn smoke() -> crate::Result<()> { 84 | let content_len = ContentLength::new(12); 85 | 86 | let mut headers = Headers::new(); 87 | content_len.apply_header(&mut headers); 88 | 89 | let content_len = ContentLength::from_headers(headers)?.unwrap(); 90 | assert_eq!(content_len.len(), 12); 91 | Ok(()) 92 | } 93 | 94 | #[test] 95 | fn bad_request_on_parse_error() { 96 | let mut headers = Headers::new(); 97 | headers 98 | .insert(CONTENT_LENGTH, "") 99 | .unwrap(); 100 | let err = ContentLength::from_headers(headers).unwrap_err(); 101 | assert_eq!(err.status(), 400); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/content/content_location.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, CONTENT_LOCATION}; 2 | use crate::{bail_status as bail, Status, Url}; 3 | 4 | use std::convert::TryInto; 5 | 6 | /// Indicates an alternate location for the returned data. 7 | /// 8 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Location) 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7231, section 3.1.4.2: Content-Location](https://tools.ietf.org/html/rfc7231#section-3.1.4.2) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::{Response, Url}; 20 | /// use http_types::content::ContentLocation; 21 | /// 22 | /// let content_location = ContentLocation::new(Url::parse("https://example.net/")?); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&content_location, &content_location); 26 | /// 27 | /// let url = Url::parse("https://example.net/")?; 28 | /// let content_location = ContentLocation::from_headers(url, res)?.unwrap(); 29 | /// assert_eq!(content_location.location(), &Url::parse("https://example.net/")?); 30 | /// # 31 | /// # Ok(()) } 32 | /// ``` 33 | #[derive(Debug)] 34 | pub struct ContentLocation { 35 | url: Url, 36 | } 37 | 38 | impl ContentLocation { 39 | /// Create a new instance of `Content-Location` header. 40 | pub fn new(url: Url) -> Self { 41 | Self { url } 42 | } 43 | 44 | /// Create a new instance from headers. 45 | pub fn from_headers(base_url: U, headers: impl AsRef) -> crate::Result> 46 | where 47 | U: TryInto, 48 | U::Error: std::fmt::Debug, 49 | { 50 | let headers = match headers.as_ref().get(CONTENT_LOCATION) { 51 | Some(headers) => headers, 52 | None => return Ok(None), 53 | }; 54 | 55 | // If we successfully parsed the header then there's always at least one 56 | // entry. We want the last entry. 57 | let value = headers.iter().last().unwrap(); 58 | let base = match base_url.try_into() { 59 | Ok(b) => b, 60 | Err(_) => bail!(400, "Invalid base url provided"), 61 | }; 62 | 63 | let url = base.join(value.as_str().trim()).status(400)?; 64 | Ok(Some(Self { url })) 65 | } 66 | 67 | /// Get the url. 68 | pub fn location(&self) -> &Url { 69 | &self.url 70 | } 71 | 72 | /// Set the url. 73 | pub fn set_location(&mut self, location: U) 74 | where 75 | U: TryInto, 76 | U::Error: std::fmt::Debug, 77 | { 78 | self.url = location 79 | .try_into() 80 | .expect("Could not convert into valid URL") 81 | } 82 | } 83 | 84 | impl Header for ContentLocation { 85 | fn header_name(&self) -> HeaderName { 86 | CONTENT_LOCATION 87 | } 88 | fn header_value(&self) -> HeaderValue { 89 | let output = self.url.to_string(); 90 | 91 | // SAFETY: the internal string is validated to be ASCII. 92 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod test { 98 | use super::*; 99 | use crate::headers::Headers; 100 | 101 | #[test] 102 | fn smoke() -> crate::Result<()> { 103 | let content_location = ContentLocation::new(Url::parse("https://example.net/test.json")?); 104 | 105 | let mut headers = Headers::new(); 106 | content_location.apply_header(&mut headers); 107 | 108 | let content_location = 109 | ContentLocation::from_headers(Url::parse("https://example.net/").unwrap(), headers)? 110 | .unwrap(); 111 | assert_eq!( 112 | content_location.location(), 113 | &Url::parse("https://example.net/test.json")? 114 | ); 115 | Ok(()) 116 | } 117 | 118 | #[test] 119 | fn bad_request_on_parse_error() { 120 | let mut headers = Headers::new(); 121 | headers 122 | .insert(CONTENT_LOCATION, "htt://") 123 | .unwrap(); 124 | let err = 125 | ContentLocation::from_headers(Url::parse("https://example.net").unwrap(), headers) 126 | .unwrap_err(); 127 | assert_eq!(err.status(), 400); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/content/content_type.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryInto, str::FromStr}; 2 | 3 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, CONTENT_TYPE}; 4 | use crate::mime::Mime; 5 | 6 | /// Indicate the media type of a resource's content. 7 | /// 8 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7231, section 3.1.1.5: Content-Type](https://tools.ietf.org/html/rfc7231#section-3.1.1.5) 13 | /// - [RFC 7233, section 4.1: Content-Type in multipart](https://tools.ietf.org/html/rfc7233#section-4.1) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::content::ContentType; 21 | /// use http_types::{headers::Header, Response}; 22 | /// use http_types::mime::Mime; 23 | /// use std::str::FromStr; 24 | /// 25 | /// let content_type = ContentType::new("text/*"); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&content_type, &content_type); 29 | /// 30 | /// let content_type = ContentType::from_headers(res)?.unwrap(); 31 | /// assert_eq!(content_type.header_value(), format!("{}", Mime::from_str("text/*")?).as_str()); 32 | /// # 33 | /// # Ok(()) } 34 | /// ``` 35 | #[derive(Debug)] 36 | pub struct ContentType { 37 | media_type: Mime, 38 | } 39 | 40 | impl ContentType { 41 | /// Create a new instance. 42 | pub fn new(media_type: U) -> Self 43 | where 44 | U: TryInto, 45 | U::Error: std::fmt::Debug, 46 | { 47 | Self { 48 | media_type: media_type 49 | .try_into() 50 | .expect("could not convert into a valid Mime type"), 51 | } 52 | } 53 | 54 | /// Create a new instance from headers. 55 | /// 56 | /// `Content-Type` headers can provide both full and partial URLs. In 57 | /// order to always return fully qualified URLs, a base URL must be passed to 58 | /// reference the current environment. In HTTP/1.1 and above this value can 59 | /// always be determined from the request. 60 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 61 | let headers = match headers.as_ref().get(CONTENT_TYPE) { 62 | Some(headers) => headers, 63 | None => return Ok(None), 64 | }; 65 | 66 | // If we successfully parsed the header then there's always at least one 67 | // entry. We want the last entry. 68 | let ctation = headers.iter().last().unwrap(); 69 | 70 | let media_type = Mime::from_str(ctation.as_str()).map_err(|mut e| { 71 | e.set_status(400); 72 | e 73 | })?; 74 | Ok(Some(Self { media_type })) 75 | } 76 | } 77 | 78 | impl Header for ContentType { 79 | fn header_name(&self) -> HeaderName { 80 | CONTENT_TYPE 81 | } 82 | fn header_value(&self) -> HeaderValue { 83 | let output = format!("{}", self.media_type); 84 | // SAFETY: the internal string is validated to be ASCII. 85 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 86 | } 87 | } 88 | 89 | impl PartialEq for ContentType { 90 | fn eq(&self, other: &Mime) -> bool { 91 | &self.media_type == other 92 | } 93 | } 94 | 95 | impl PartialEq<&Mime> for ContentType { 96 | fn eq(&self, other: &&Mime) -> bool { 97 | &&self.media_type == other 98 | } 99 | } 100 | 101 | impl From for ContentType { 102 | fn from(media_type: Mime) -> Self { 103 | Self { media_type } 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod test { 109 | use super::*; 110 | use crate::headers::Headers; 111 | 112 | #[test] 113 | fn smoke() -> crate::Result<()> { 114 | let ct = ContentType::new(Mime::from_str("text/*")?); 115 | 116 | let mut headers = Headers::new(); 117 | ct.apply_header(&mut headers); 118 | 119 | let ct = ContentType::from_headers(headers)?.unwrap(); 120 | assert_eq!( 121 | ct.header_value(), 122 | format!("{}", Mime::from_str("text/*")?).as_str() 123 | ); 124 | Ok(()) 125 | } 126 | 127 | #[test] 128 | fn bad_request_on_parse_error() { 129 | let mut headers = Headers::new(); 130 | headers 131 | .insert(CONTENT_TYPE, "") 132 | .unwrap(); 133 | let err = ContentType::from_headers(headers).unwrap_err(); 134 | assert_eq!(err.status(), 400); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/content/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::HeaderValue; 2 | use std::fmt::{self, Display}; 3 | 4 | /// Available compression algorithms. 5 | #[non_exhaustive] 6 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 7 | pub enum Encoding { 8 | /// The Gzip encoding. 9 | Gzip, 10 | /// The Deflate encoding. 11 | Deflate, 12 | /// The Brotli encoding. 13 | Brotli, 14 | /// The Zstd encoding. 15 | Zstd, 16 | /// No encoding. 17 | Identity, 18 | } 19 | 20 | impl Encoding { 21 | /// Parses a given string into its corresponding encoding. 22 | pub(crate) fn from_str(s: &str) -> Option { 23 | let s = s.trim(); 24 | 25 | // We're dealing with an empty string. 26 | if s.is_empty() { 27 | return None; 28 | } 29 | 30 | match s { 31 | "gzip" => Some(Encoding::Gzip), 32 | "deflate" => Some(Encoding::Deflate), 33 | "br" => Some(Encoding::Brotli), 34 | "zstd" => Some(Encoding::Zstd), 35 | "identity" => Some(Encoding::Identity), 36 | _ => None, 37 | } 38 | } 39 | } 40 | 41 | impl Display for Encoding { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | match self { 44 | Encoding::Gzip => write!(f, "gzip"), 45 | Encoding::Deflate => write!(f, "deflate"), 46 | Encoding::Brotli => write!(f, "br"), 47 | Encoding::Zstd => write!(f, "zstd"), 48 | Encoding::Identity => write!(f, "identity"), 49 | } 50 | } 51 | } 52 | 53 | impl From for HeaderValue { 54 | fn from(directive: Encoding) -> Self { 55 | let s = directive.to_string(); 56 | unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/content/encoding_proposal.rs: -------------------------------------------------------------------------------- 1 | use crate::content::Encoding; 2 | use crate::ensure; 3 | use crate::headers::HeaderValue; 4 | use crate::utils::parse_weight; 5 | 6 | use std::cmp::{Ordering, PartialEq}; 7 | use std::ops::{Deref, DerefMut}; 8 | 9 | /// A proposed `Encoding` in `AcceptEncoding`. 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | pub struct EncodingProposal { 12 | /// The proposed encoding. 13 | pub(crate) encoding: Encoding, 14 | 15 | /// The weight of the proposal. 16 | /// 17 | /// This is a number between 0.0 and 1.0, and is max 3 decimal points. 18 | weight: Option, 19 | } 20 | 21 | impl EncodingProposal { 22 | /// Create a new instance of `EncodingProposal`. 23 | pub fn new(encoding: impl Into, weight: Option) -> crate::Result { 24 | if let Some(weight) = weight { 25 | ensure!( 26 | weight.is_sign_positive() && weight <= 1.0, 27 | "EncodingProposal should have a weight between 0.0 and 1.0" 28 | ) 29 | } 30 | 31 | Ok(Self { 32 | encoding: encoding.into(), 33 | weight, 34 | }) 35 | } 36 | 37 | /// Get the proposed encoding. 38 | pub fn encoding(&self) -> &Encoding { 39 | &self.encoding 40 | } 41 | 42 | /// Get the weight of the proposal. 43 | pub fn weight(&self) -> Option { 44 | self.weight 45 | } 46 | 47 | pub(crate) fn from_str(s: &str) -> crate::Result> { 48 | let mut parts = s.split(';'); 49 | let encoding = match Encoding::from_str(parts.next().unwrap()) { 50 | Some(encoding) => encoding, 51 | None => return Ok(None), 52 | }; 53 | let weight = parts.next().map(parse_weight).transpose()?; 54 | 55 | Ok(Some(Self::new(encoding, weight)?)) 56 | } 57 | } 58 | 59 | impl From for EncodingProposal { 60 | fn from(encoding: Encoding) -> Self { 61 | Self { 62 | encoding, 63 | weight: None, 64 | } 65 | } 66 | } 67 | 68 | impl PartialEq for EncodingProposal { 69 | fn eq(&self, other: &Encoding) -> bool { 70 | self.encoding == *other 71 | } 72 | } 73 | 74 | impl PartialEq for &EncodingProposal { 75 | fn eq(&self, other: &Encoding) -> bool { 76 | self.encoding == *other 77 | } 78 | } 79 | 80 | impl Deref for EncodingProposal { 81 | type Target = Encoding; 82 | fn deref(&self) -> &Self::Target { 83 | &self.encoding 84 | } 85 | } 86 | 87 | impl DerefMut for EncodingProposal { 88 | fn deref_mut(&mut self) -> &mut Self::Target { 89 | &mut self.encoding 90 | } 91 | } 92 | 93 | // NOTE: Firefox populates Accept-Encoding as `gzip, deflate, br`. This means 94 | // when parsing encodings we should choose the last value in the list under 95 | // equal weights. This impl doesn't know which value was passed later, so that 96 | // behavior needs to be handled separately. 97 | // 98 | // NOTE: This comparison does not include a notion of `*` (any value is valid). 99 | // that needs to be handled separately. 100 | impl PartialOrd for EncodingProposal { 101 | fn partial_cmp(&self, other: &Self) -> Option { 102 | match (self.weight, other.weight) { 103 | (Some(left), Some(right)) => left.partial_cmp(&right), 104 | (Some(_), None) => Some(Ordering::Greater), 105 | (None, Some(_)) => Some(Ordering::Less), 106 | (None, None) => None, 107 | } 108 | } 109 | } 110 | 111 | impl From for HeaderValue { 112 | fn from(entry: EncodingProposal) -> HeaderValue { 113 | let s = match entry.weight { 114 | Some(weight) => format!("{};q={:.3}", entry.encoding, weight), 115 | None => entry.encoding.to_string(), 116 | }; 117 | unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod test { 123 | use super::*; 124 | 125 | #[test] 126 | fn smoke() { 127 | let _ = EncodingProposal::new(Encoding::Gzip, Some(0.0)).unwrap(); 128 | let _ = EncodingProposal::new(Encoding::Gzip, Some(0.5)).unwrap(); 129 | let _ = EncodingProposal::new(Encoding::Gzip, Some(1.0)).unwrap(); 130 | } 131 | 132 | #[test] 133 | fn error_code_500() { 134 | let err = EncodingProposal::new(Encoding::Gzip, Some(1.1)).unwrap_err(); 135 | assert_eq!(err.status(), 500); 136 | 137 | let err = EncodingProposal::new(Encoding::Gzip, Some(-0.1)).unwrap_err(); 138 | assert_eq!(err.status(), 500); 139 | 140 | let err = EncodingProposal::new(Encoding::Gzip, Some(-0.0)).unwrap_err(); 141 | assert_eq!(err.status(), 500); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/content/media_type_proposal.rs: -------------------------------------------------------------------------------- 1 | use crate::ensure; 2 | use crate::headers::HeaderValue; 3 | use crate::mime::Mime; 4 | 5 | use std::ops::{Deref, DerefMut}; 6 | use std::{ 7 | cmp::{Ordering, PartialEq}, 8 | str::FromStr, 9 | }; 10 | 11 | /// A proposed Media Type for the `Accept` header. 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct MediaTypeProposal { 14 | /// The proposed media_type. 15 | pub(crate) media_type: Mime, 16 | 17 | /// The weight of the proposal. 18 | /// 19 | /// This is a number between 0.0 and 1.0, and is max 3 decimal points. 20 | weight: Option, 21 | } 22 | 23 | impl MediaTypeProposal { 24 | /// Create a new instance of `MediaTypeProposal`. 25 | pub fn new(media_type: impl Into, weight: Option) -> crate::Result { 26 | if let Some(weight) = weight { 27 | ensure!( 28 | weight.is_sign_positive() && weight <= 1.0, 29 | "MediaTypeProposal should have a weight between 0.0 and 1.0" 30 | ) 31 | } 32 | if weight.is_none() { 33 | return Ok(Self { 34 | media_type: media_type.into(), 35 | weight: Some(1.0), 36 | }); 37 | } 38 | 39 | Ok(Self { 40 | media_type: media_type.into(), 41 | weight, 42 | }) 43 | } 44 | 45 | /// Get the proposed media_type. 46 | pub fn media_type(&self) -> &Mime { 47 | &self.media_type 48 | } 49 | 50 | /// Get the weight of the proposal. 51 | pub fn weight(&self) -> Option { 52 | self.weight 53 | } 54 | 55 | /// Parse a string into a media type proposal. 56 | /// 57 | /// Because `;` and `q=0.0` are all valid values for in use in a media type, 58 | /// we have to parse the full string to the media type first, and then see if 59 | /// a `q` value has been set. 60 | pub(crate) fn from_str(s: &str) -> crate::Result { 61 | let mut media_type = Mime::from_str(s)?; 62 | let weight = media_type 63 | .remove_param("q") 64 | .map(|param| param.as_str().parse()) 65 | .transpose()?; 66 | Self::new(media_type, weight) 67 | } 68 | } 69 | 70 | impl From for MediaTypeProposal { 71 | fn from(media_type: Mime) -> Self { 72 | Self { 73 | media_type, 74 | weight: None, 75 | } 76 | } 77 | } 78 | 79 | impl From for Mime { 80 | fn from(accept: MediaTypeProposal) -> Self { 81 | accept.media_type 82 | } 83 | } 84 | 85 | impl PartialEq for MediaTypeProposal { 86 | fn eq(&self, other: &Mime) -> bool { 87 | self.media_type == *other 88 | } 89 | } 90 | 91 | impl PartialEq for &MediaTypeProposal { 92 | fn eq(&self, other: &Mime) -> bool { 93 | self.media_type == *other 94 | } 95 | } 96 | 97 | impl Deref for MediaTypeProposal { 98 | type Target = Mime; 99 | fn deref(&self) -> &Self::Target { 100 | &self.media_type 101 | } 102 | } 103 | 104 | impl DerefMut for MediaTypeProposal { 105 | fn deref_mut(&mut self) -> &mut Self::Target { 106 | &mut self.media_type 107 | } 108 | } 109 | 110 | // NOTE: For Accept-Encoding Firefox sends the values: `gzip, deflate, br`. This means 111 | // when parsing media_types we should choose the last value in the list under 112 | // equal weights. This impl doesn't know which value was passed later, so that 113 | // behavior needs to be handled separately. 114 | // 115 | // NOTE: This comparison does not include a notion of `*` (any value is valid). 116 | // that needs to be handled separately. 117 | impl PartialOrd for MediaTypeProposal { 118 | fn partial_cmp(&self, other: &Self) -> Option { 119 | match (self.weight, other.weight) { 120 | (Some(left), Some(right)) => left.partial_cmp(&right), 121 | (Some(_), None) => Some(Ordering::Greater), 122 | (None, Some(_)) => Some(Ordering::Less), 123 | (None, None) => None, 124 | } 125 | } 126 | } 127 | 128 | impl From for HeaderValue { 129 | fn from(entry: MediaTypeProposal) -> HeaderValue { 130 | let s = match entry.weight { 131 | Some(weight) => format!("{};q={:.3}", entry.media_type, weight), 132 | None => entry.media_type.to_string(), 133 | }; 134 | unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } 135 | } 136 | } 137 | 138 | #[cfg(test)] 139 | mod test { 140 | use super::*; 141 | use crate::mime; 142 | 143 | #[test] 144 | fn smoke() { 145 | let _ = MediaTypeProposal::new(mime::JSON, Some(0.0)).unwrap(); 146 | let _ = MediaTypeProposal::new(mime::XML, Some(0.5)).unwrap(); 147 | let _ = MediaTypeProposal::new(mime::HTML, Some(1.0)).unwrap(); 148 | } 149 | 150 | #[test] 151 | fn error_code_500() { 152 | let err = MediaTypeProposal::new(mime::JSON, Some(1.1)).unwrap_err(); 153 | assert_eq!(err.status(), 500); 154 | 155 | let err = MediaTypeProposal::new(mime::XML, Some(-0.1)).unwrap_err(); 156 | assert_eq!(err.status(), 500); 157 | 158 | let err = MediaTypeProposal::new(mime::HTML, Some(-0.0)).unwrap_err(); 159 | assert_eq!(err.status(), 500); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/content/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Content headers. 2 | //! 3 | //! These headers are used for "content negotiation": the client shares information 4 | //! about which content it prefers, and the server responds by sharing which 5 | //! content it's chosen to share. This enables clients to receive resources with the 6 | //! best available compression, in the preferred language, and more. 7 | //! 8 | //! # Further Reading 9 | //! 10 | //! - [MDN: Content Negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) 11 | //! 12 | //! # Examples 13 | //! 14 | //! ``` 15 | //! # fn main() -> http_types::Result<()> { 16 | //! # 17 | //! use http_types::content::{Accept, MediaTypeProposal}; 18 | //! use http_types::{mime, Response}; 19 | //! 20 | //! let mut accept = Accept::new(); 21 | //! accept.push(MediaTypeProposal::new(mime::HTML, Some(0.8))?); 22 | //! accept.push(MediaTypeProposal::new(mime::XML, Some(0.4))?); 23 | //! accept.push(mime::PLAIN); 24 | //! 25 | //! let mut res = Response::new(200); 26 | //! let content_type = accept.negotiate(&[mime::XML])?; 27 | //! res.insert_header(&content_type, &content_type); 28 | //! 29 | //! assert_eq!(res["Content-Type"], "application/xml;charset=utf-8"); 30 | //! # 31 | //! # Ok(()) } 32 | //! ``` 33 | 34 | pub mod accept; 35 | pub mod accept_encoding; 36 | pub mod content_encoding; 37 | 38 | mod content_length; 39 | mod content_location; 40 | mod content_type; 41 | mod encoding; 42 | mod encoding_proposal; 43 | mod media_type_proposal; 44 | 45 | #[doc(inline)] 46 | pub use accept::Accept; 47 | #[doc(inline)] 48 | pub use accept_encoding::AcceptEncoding; 49 | #[doc(inline)] 50 | pub use content_encoding::ContentEncoding; 51 | pub use content_length::ContentLength; 52 | pub use content_location::ContentLocation; 53 | pub use content_type::ContentType; 54 | pub use encoding::Encoding; 55 | pub use encoding_proposal::EncodingProposal; 56 | pub use media_type_proposal::MediaTypeProposal; 57 | -------------------------------------------------------------------------------- /src/extensions.rs: -------------------------------------------------------------------------------- 1 | // Implementation is based on 2 | // - https://github.com/hyperium/http/blob/master/src/extensions.rs 3 | // - https://github.com/kardeiz/type-map/blob/master/src/lib.rs 4 | 5 | use std::any::{Any, TypeId}; 6 | use std::collections::HashMap; 7 | use std::fmt; 8 | use std::hash::{BuildHasherDefault, Hasher}; 9 | 10 | /// A type to store extra data inside `Request` and `Response`. 11 | /// 12 | /// Store and retrieve values by 13 | /// [`TypeId`](https://doc.rust-lang.org/std/any/struct.TypeId.html). This allows 14 | /// storing arbitrary data that implements `Sync + Send + 'static`. This is 15 | /// useful when for example implementing middleware that needs to send values. 16 | #[derive(Default)] 17 | pub struct Extensions { 18 | map: Option, BuildHasherDefault>>, 19 | } 20 | 21 | impl Extensions { 22 | /// Create an empty `Extensions`. 23 | #[inline] 24 | pub(crate) fn new() -> Self { 25 | Self { map: None } 26 | } 27 | 28 | /// Insert a value into this `Extensions`. 29 | /// 30 | /// If a value of this type already exists, it will be returned. 31 | pub fn insert(&mut self, val: T) -> Option { 32 | self.map 33 | .get_or_insert_with(Default::default) 34 | .insert(TypeId::of::(), Box::new(val)) 35 | .and_then(|boxed| (boxed as Box).downcast().ok().map(|boxed| *boxed)) 36 | } 37 | 38 | /// Check if container contains value for type 39 | pub fn contains(&self) -> bool { 40 | self.map 41 | .as_ref() 42 | .and_then(|m| m.get(&TypeId::of::())) 43 | .is_some() 44 | } 45 | 46 | /// Get a reference to a value previously inserted on this `Extensions`. 47 | pub fn get(&self) -> Option<&T> { 48 | self.map 49 | .as_ref() 50 | .and_then(|m| m.get(&TypeId::of::())) 51 | .and_then(|boxed| (&**boxed as &(dyn Any)).downcast_ref()) 52 | } 53 | 54 | /// Get a mutable reference to a value previously inserted on this `Extensions`. 55 | pub fn get_mut(&mut self) -> Option<&mut T> { 56 | self.map 57 | .as_mut() 58 | .and_then(|m| m.get_mut(&TypeId::of::())) 59 | .and_then(|boxed| (&mut **boxed as &mut (dyn Any)).downcast_mut()) 60 | } 61 | 62 | /// Remove a value from this `Extensions`. 63 | /// 64 | /// If a value of this type exists, it will be returned. 65 | pub fn remove(&mut self) -> Option { 66 | self.map 67 | .as_mut() 68 | .and_then(|m| m.remove(&TypeId::of::())) 69 | .and_then(|boxed| (boxed as Box).downcast().ok().map(|boxed| *boxed)) 70 | } 71 | 72 | /// Clear the `Extensions` of all inserted values. 73 | #[inline] 74 | pub fn clear(&mut self) { 75 | self.map = None; 76 | } 77 | } 78 | 79 | impl fmt::Debug for Extensions { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | f.debug_struct("Extensions").finish() 82 | } 83 | } 84 | 85 | // With TypeIds as keys, there's no need to hash them. So we simply use an identy hasher. 86 | #[derive(Default)] 87 | struct IdHasher(u64); 88 | 89 | impl Hasher for IdHasher { 90 | fn write(&mut self, _: &[u8]) { 91 | unreachable!("TypeId calls write_u64"); 92 | } 93 | 94 | #[inline] 95 | fn write_u64(&mut self, id: u64) { 96 | self.0 = id; 97 | } 98 | 99 | #[inline] 100 | fn finish(&self) -> u64 { 101 | self.0 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | #[test] 109 | fn test_extensions() { 110 | #[derive(Debug, PartialEq)] 111 | struct MyType(i32); 112 | 113 | let mut map = Extensions::new(); 114 | 115 | map.insert(5i32); 116 | map.insert(MyType(10)); 117 | 118 | assert_eq!(map.get(), Some(&5i32)); 119 | assert_eq!(map.get_mut(), Some(&mut 5i32)); 120 | 121 | assert_eq!(map.remove::(), Some(5i32)); 122 | assert!(map.get::().is_none()); 123 | 124 | assert_eq!(map.get::(), None); 125 | assert_eq!(map.get(), Some(&MyType(10))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/headers/header.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use crate::headers::{HeaderName, HeaderValue, Headers}; 4 | 5 | /// A trait representing a [`HeaderName`] and [`HeaderValue`] pair. 6 | pub trait Header { 7 | /// Access the header's name. 8 | fn header_name(&self) -> HeaderName; 9 | 10 | /// Access the header's value. 11 | fn header_value(&self) -> HeaderValue; 12 | 13 | /// Insert the header name and header value into something that looks like a 14 | /// [`Headers`] map. 15 | fn apply_header>(&self, mut headers: H) { 16 | let name = self.header_name(); 17 | let value = self.header_value(); 18 | 19 | // The value should already have been validated when it was created, so this should 20 | // possibly be done with an unsafe call 21 | headers.as_mut().insert(name, value).unwrap(); 22 | } 23 | } 24 | 25 | impl<'a, 'b> Header for (&'a str, &'b str) { 26 | fn header_name(&self) -> HeaderName { 27 | HeaderName::from(self.0) 28 | } 29 | 30 | fn header_value(&self) -> HeaderValue { 31 | HeaderValue::from_bytes(self.1.to_owned().into_bytes()) 32 | .expect("String slice should be valid ASCII") 33 | } 34 | } 35 | 36 | impl<'a, T: Header> Header for &'a T { 37 | fn header_name(&self) -> HeaderName { 38 | self.deref().header_name() 39 | } 40 | 41 | fn header_value(&self) -> HeaderValue { 42 | self.deref().header_value() 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod test { 48 | use super::*; 49 | 50 | #[test] 51 | fn header_from_strings() { 52 | let strings = ("Content-Length", "12"); 53 | assert_eq!(strings.header_name(), "Content-Length"); 54 | assert_eq!(strings.header_value(), "12"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/headers/header_name.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::{self, Debug, Display}; 3 | use std::str::FromStr; 4 | 5 | use crate::Error; 6 | 7 | use super::Header; 8 | 9 | /// A header name. 10 | #[derive(Clone, PartialEq, Eq, Hash)] 11 | pub struct HeaderName(Cow<'static, str>); 12 | 13 | impl HeaderName { 14 | /// Create a new `HeaderName` from a Vec of ASCII bytes. 15 | /// 16 | /// # Error 17 | /// 18 | /// This function will error if the bytes is not valid ASCII. 19 | pub fn from_bytes(mut bytes: Vec) -> Result { 20 | crate::ensure!(bytes.is_ascii(), "Bytes should be valid ASCII"); 21 | bytes.make_ascii_lowercase(); 22 | 23 | // This is permitted because ASCII is valid UTF-8, and we just checked that. 24 | let string = unsafe { String::from_utf8_unchecked(bytes.to_vec()) }; 25 | Ok(HeaderName(Cow::Owned(string))) 26 | } 27 | 28 | /// Create a new `HeaderName` from an ASCII string. 29 | /// 30 | /// # Error 31 | /// 32 | /// This function will error if the string is not valid ASCII. 33 | pub fn from_string(s: String) -> Result { 34 | Self::from_bytes(s.into_bytes()) 35 | } 36 | 37 | /// Returns the header name as a `&str`. 38 | pub fn as_str(&self) -> &'_ str { 39 | &self.0 40 | } 41 | 42 | /// Converts a vector of bytes to a `HeaderName` without checking that the string contains 43 | /// valid ASCII. 44 | /// 45 | /// # Safety 46 | /// 47 | /// This function is unsafe because it does not check that the bytes passed to it are valid 48 | /// ASCII. If this constraint is violated, it may cause memory 49 | /// unsafety issues with future users of the HeaderName, as the rest of the library assumes 50 | /// that Strings are valid ASCII. 51 | pub unsafe fn from_bytes_unchecked(mut bytes: Vec) -> Self { 52 | bytes.make_ascii_lowercase(); 53 | let string = String::from_utf8_unchecked(bytes); 54 | HeaderName(Cow::Owned(string)) 55 | } 56 | 57 | /// Converts a string assumed to lowercase into a `HeaderName` 58 | pub(crate) const fn from_lowercase_str(str: &'static str) -> Self { 59 | HeaderName(Cow::Borrowed(str)) 60 | } 61 | } 62 | 63 | impl Debug for HeaderName { 64 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 65 | write!(f, "{:?}", self.0) 66 | } 67 | } 68 | 69 | impl Display for HeaderName { 70 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 71 | write!(f, "{}", self.0) 72 | } 73 | } 74 | 75 | impl FromStr for HeaderName { 76 | type Err = Error; 77 | 78 | /// Create a new `HeaderName`. 79 | /// 80 | /// This checks it's valid ASCII, and lowercases it. 81 | fn from_str(s: &str) -> Result { 82 | crate::ensure!(s.is_ascii(), "String slice should be valid ASCII"); 83 | Ok(HeaderName(Cow::Owned(s.to_ascii_lowercase()))) 84 | } 85 | } 86 | 87 | impl From<&HeaderName> for HeaderName { 88 | fn from(value: &HeaderName) -> HeaderName { 89 | value.clone() 90 | } 91 | } 92 | 93 | impl From for HeaderName { 94 | fn from(header: T) -> HeaderName { 95 | header.header_name() 96 | } 97 | } 98 | 99 | impl<'a> From<&'a str> for HeaderName { 100 | fn from(value: &'a str) -> Self { 101 | Self::from_str(value).expect("String slice should be valid ASCII") 102 | } 103 | } 104 | 105 | impl PartialEq for HeaderName { 106 | fn eq(&self, other: &str) -> bool { 107 | match HeaderName::from_str(other) { 108 | Err(_) => false, 109 | Ok(other) => self == &other, 110 | } 111 | } 112 | } 113 | 114 | impl<'a> PartialEq<&'a str> for HeaderName { 115 | fn eq(&self, other: &&'a str) -> bool { 116 | match HeaderName::from_str(other) { 117 | Err(_) => false, 118 | Ok(other) => self == &other, 119 | } 120 | } 121 | } 122 | 123 | impl PartialEq for HeaderName { 124 | fn eq(&self, other: &String) -> bool { 125 | match HeaderName::from_str(other) { 126 | Err(_) => false, 127 | Ok(other) => self == &other, 128 | } 129 | } 130 | } 131 | 132 | impl PartialEq<&String> for HeaderName { 133 | fn eq(&self, other: &&String) -> bool { 134 | match HeaderName::from_str(other) { 135 | Err(_) => false, 136 | Ok(other) => self == &other, 137 | } 138 | } 139 | } 140 | 141 | #[cfg(test)] 142 | mod tests { 143 | use super::*; 144 | 145 | #[test] 146 | #[allow(clippy::eq_op)] 147 | fn test_header_name_static_non_static() { 148 | let static_header = HeaderName::from_lowercase_str("hello"); 149 | let non_static_header = HeaderName::from_str("hello").unwrap(); 150 | 151 | assert_eq!(&static_header, &non_static_header); 152 | assert_eq!(&static_header, &static_header); 153 | assert_eq!(&non_static_header, &non_static_header); 154 | 155 | assert_eq!(static_header, non_static_header); 156 | assert_eq!(static_header, static_header); 157 | assert_eq!(non_static_header, non_static_header); 158 | } 159 | 160 | #[test] 161 | fn equality() { 162 | let static_header = HeaderName::from_lowercase_str("hello"); 163 | assert_eq!(static_header, "hello"); 164 | assert_eq!(&static_header, "hello"); 165 | assert_eq!(static_header, String::from("hello")); 166 | assert_eq!(static_header, &String::from("hello")); 167 | 168 | // Must validate regardless of casing. 169 | assert_eq!(static_header, &String::from("Hello")); 170 | } 171 | 172 | #[test] 173 | fn pass_name_by_ref() { 174 | let mut res = crate::Response::new(200); 175 | res.insert_header(&crate::headers::HOST, "127.0.0.1") 176 | .unwrap(); 177 | } 178 | 179 | #[test] 180 | fn test_debug() { 181 | let header_name = HeaderName::from_str("hello").unwrap(); 182 | assert_eq!(format!("{:?}", header_name), "\"hello\""); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/headers/header_value.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt::{self, Debug, Display}; 3 | use std::str::FromStr; 4 | 5 | #[cfg(feature = "cookies")] 6 | use crate::cookies::Cookie; 7 | use crate::headers::HeaderValues; 8 | use crate::mime::Mime; 9 | use crate::Error; 10 | 11 | /// A header value. 12 | #[derive(Clone, Eq, PartialEq, Hash)] 13 | pub struct HeaderValue { 14 | inner: String, 15 | } 16 | 17 | impl HeaderValue { 18 | /// Create a new `HeaderValue` from a Vec of ASCII bytes. 19 | /// 20 | /// # Error 21 | /// 22 | /// This function will error if the bytes is not valid ASCII. 23 | pub fn from_bytes(bytes: Vec) -> Result { 24 | crate::ensure!(bytes.is_ascii(), "Bytes should be valid ASCII"); 25 | 26 | // This is permitted because ASCII is valid UTF-8, and we just checked that. 27 | let string = unsafe { String::from_utf8_unchecked(bytes) }; 28 | Ok(Self { inner: string }) 29 | } 30 | 31 | /// Converts a vector of bytes to a `HeaderValue` without checking that the string contains 32 | /// valid ASCII. 33 | /// 34 | /// # Safety 35 | /// 36 | /// This function is unsafe because it does not check that the bytes passed to it are valid 37 | /// ASCII. If this constraint is violated, it may cause memory 38 | /// unsafety issues with future users of the HeaderValue, as the rest of the library assumes 39 | /// that Strings are valid ASCII. 40 | pub unsafe fn from_bytes_unchecked(bytes: Vec) -> Self { 41 | let string = String::from_utf8_unchecked(bytes); 42 | Self { inner: string } 43 | } 44 | 45 | /// Get the header value as a `&str` 46 | pub fn as_str(&self) -> &str { 47 | &self.inner 48 | } 49 | } 50 | 51 | impl From for HeaderValue { 52 | fn from(mime: Mime) -> Self { 53 | HeaderValue { 54 | inner: format!("{}", mime), 55 | } 56 | } 57 | } 58 | 59 | #[cfg(feature = "cookies")] 60 | impl From> for HeaderValue { 61 | fn from(cookie: Cookie<'_>) -> Self { 62 | HeaderValue { 63 | inner: cookie.to_string(), 64 | } 65 | } 66 | } 67 | 68 | impl From<&Mime> for HeaderValue { 69 | fn from(mime: &Mime) -> Self { 70 | HeaderValue { 71 | inner: format!("{}", mime), 72 | } 73 | } 74 | } 75 | 76 | impl FromStr for HeaderValue { 77 | type Err = Error; 78 | 79 | /// Create a new `HeaderValue`. 80 | /// 81 | /// This checks it's valid ASCII. 82 | fn from_str(s: &str) -> Result { 83 | crate::ensure!(s.is_ascii(), "String slice should be valid ASCII"); 84 | Ok(Self { 85 | inner: String::from(s), 86 | }) 87 | } 88 | } 89 | 90 | impl<'a> TryFrom<&'a str> for HeaderValue { 91 | type Error = Error; 92 | 93 | fn try_from(value: &'a str) -> Result { 94 | Self::from_str(value) 95 | } 96 | } 97 | 98 | impl Debug for HeaderValue { 99 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 | write!(f, "{:?}", self.inner) 101 | } 102 | } 103 | 104 | impl Display for HeaderValue { 105 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 106 | write!(f, "{}", self.inner) 107 | } 108 | } 109 | 110 | impl PartialEq for HeaderValue { 111 | fn eq(&self, other: &str) -> bool { 112 | self.inner == other 113 | } 114 | } 115 | 116 | impl<'a> PartialEq<&'a str> for HeaderValue { 117 | fn eq(&self, other: &&'a str) -> bool { 118 | &self.inner == other 119 | } 120 | } 121 | 122 | impl PartialEq for HeaderValue { 123 | fn eq(&self, other: &String) -> bool { 124 | &self.inner == other 125 | } 126 | } 127 | 128 | impl PartialEq<&String> for HeaderValue { 129 | fn eq(&self, other: &&String) -> bool { 130 | &&self.inner == other 131 | } 132 | } 133 | 134 | impl From for HeaderValue { 135 | fn from(mut other: HeaderValues) -> Self { 136 | other.inner.reverse(); 137 | other 138 | .inner 139 | .pop() 140 | .expect("HeaderValues should contain at least one value") 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn test_debug() { 150 | let header_value = HeaderValue::from_str("foo0").unwrap(); 151 | assert_eq!(format!("{:?}", header_value), "\"foo0\""); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/headers/header_values.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{HeaderValue, Values}; 2 | 3 | use std::fmt::{self, Debug, Display}; 4 | use std::iter::FromIterator; 5 | use std::ops::{Deref, DerefMut, Index}; 6 | use std::slice::SliceIndex; 7 | 8 | /// A list of `HeaderValue`s. 9 | /// 10 | /// This always contains at least one header value. 11 | #[derive(Clone)] 12 | pub struct HeaderValues { 13 | pub(crate) inner: Vec, 14 | } 15 | 16 | impl HeaderValues { 17 | /// Move all values from `other` into `self`, leaving `other` empty. 18 | pub fn append(&mut self, other: &mut Self) { 19 | self.inner.append(&mut other.inner) 20 | } 21 | 22 | /// Returns a reference or a value depending on the type of index. 23 | pub fn get(&self, index: usize) -> Option<&HeaderValue> { 24 | self.inner.get(index) 25 | } 26 | 27 | /// Returns a mutable reference or a value depending on the type of index. 28 | pub fn get_mut(&mut self, index: usize) -> Option<&mut HeaderValue> { 29 | self.inner.get_mut(index) 30 | } 31 | 32 | /// Returns `true` if there is a value corresponding to the specified `HeaderValue` in the list, 33 | /// `false` otherwise. 34 | pub fn contains(&self, value: &HeaderValue) -> bool { 35 | self.inner.contains(value) 36 | } 37 | 38 | /// Returns the last `HeaderValue`. 39 | pub fn last(&self) -> &HeaderValue { 40 | self.inner 41 | .last() 42 | .expect("HeaderValues must always contain at least one value") 43 | } 44 | 45 | /// An iterator visiting all header values in arbitrary order. 46 | pub fn iter(&self) -> Values<'_> { 47 | Values::new_values(self) 48 | } 49 | 50 | // /// An iterator visiting all header values in arbitrary order, with mutable 51 | // /// references to the values. 52 | // pub fn iter_mut(&mut self) -> ValuesMut<'_> { 53 | // ValuesMut { 54 | // inner: self.headers.iter_mut(), 55 | // } 56 | // } 57 | } 58 | 59 | impl> Index for HeaderValues { 60 | type Output = I::Output; 61 | 62 | #[inline] 63 | fn index(&self, index: I) -> &Self::Output { 64 | Index::index(&self.inner, index) 65 | } 66 | } 67 | 68 | impl FromIterator for HeaderValues { 69 | fn from_iter(iter: I) -> HeaderValues 70 | where 71 | I: IntoIterator, 72 | { 73 | let iter = iter.into_iter(); 74 | let mut output = Vec::with_capacity(iter.size_hint().0); 75 | for v in iter { 76 | output.push(v); 77 | } 78 | HeaderValues { inner: output } 79 | } 80 | } 81 | 82 | impl Debug for HeaderValues { 83 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 84 | if self.inner.len() == 1 { 85 | write!(f, "{:?}", self.inner[0]) 86 | } else { 87 | f.debug_list().entries(self.inner.iter()).finish() 88 | } 89 | } 90 | } 91 | 92 | impl Display for HeaderValues { 93 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 94 | let mut list = f.debug_list(); 95 | for v in &self.inner { 96 | list.entry(&v); 97 | } 98 | list.finish() 99 | } 100 | } 101 | 102 | impl PartialEq for HeaderValues { 103 | fn eq(&self, other: &str) -> bool { 104 | self.inner.len() == 1 && self.inner[0] == other 105 | } 106 | } 107 | 108 | impl<'a> PartialEq<&'a str> for HeaderValues { 109 | fn eq(&self, other: &&'a str) -> bool { 110 | self.inner.len() == 1 && &self.inner[0] == other 111 | } 112 | } 113 | 114 | impl<'a> PartialEq<[&'a str]> for HeaderValues { 115 | fn eq(&self, other: &[&'a str]) -> bool { 116 | self.inner.iter().eq(other.iter()) 117 | } 118 | } 119 | 120 | impl PartialEq for HeaderValues { 121 | fn eq(&self, other: &String) -> bool { 122 | self.inner.len() == 1 && self.inner[0] == *other 123 | } 124 | } 125 | 126 | impl PartialEq<&String> for HeaderValues { 127 | fn eq(&self, other: &&String) -> bool { 128 | self.inner.len() == 1 && self.inner[0] == **other 129 | } 130 | } 131 | 132 | impl From for HeaderValues { 133 | fn from(other: HeaderValue) -> Self { 134 | Self { inner: vec![other] } 135 | } 136 | } 137 | 138 | impl AsRef for HeaderValues { 139 | fn as_ref(&self) -> &HeaderValue { 140 | &self.inner[0] 141 | } 142 | } 143 | 144 | impl AsMut for HeaderValues { 145 | fn as_mut(&mut self) -> &mut HeaderValue { 146 | &mut self.inner[0] 147 | } 148 | } 149 | 150 | impl Deref for HeaderValues { 151 | type Target = HeaderValue; 152 | 153 | fn deref(&self) -> &HeaderValue { 154 | &self.inner[0] 155 | } 156 | } 157 | 158 | impl DerefMut for HeaderValues { 159 | fn deref_mut(&mut self) -> &mut HeaderValue { 160 | &mut self.inner[0] 161 | } 162 | } 163 | 164 | impl<'a> IntoIterator for &'a HeaderValues { 165 | type Item = &'a HeaderValue; 166 | type IntoIter = Values<'a>; 167 | 168 | #[inline] 169 | fn into_iter(self) -> Self::IntoIter { 170 | self.iter() 171 | } 172 | } 173 | 174 | impl From> for HeaderValues { 175 | fn from(headers: Vec) -> Self { 176 | Self { inner: headers } 177 | } 178 | } 179 | 180 | impl IntoIterator for HeaderValues { 181 | type Item = HeaderValue; 182 | type IntoIter = std::vec::IntoIter; 183 | 184 | #[inline] 185 | fn into_iter(self) -> Self::IntoIter { 186 | self.inner.into_iter() 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | 194 | #[test] 195 | fn test_debug_single() { 196 | let header_values = HeaderValues { 197 | inner: vec!["foo0".parse().unwrap()], 198 | }; 199 | assert_eq!(format!("{:?}", header_values), "\"foo0\""); 200 | } 201 | #[test] 202 | fn test_debug_multiple() { 203 | let header_values = HeaderValues { 204 | inner: vec!["foo0".parse().unwrap(), "foo1".parse().unwrap()], 205 | }; 206 | assert_eq!(format!("{:?}", header_values), r#"["foo0", "foo1"]"#); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/headers/into_iter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::iter::Iterator; 3 | 4 | use crate::headers::{HeaderName, HeaderValues}; 5 | 6 | /// An owning iterator over the entries of `Headers`. 7 | #[derive(Debug)] 8 | pub struct IntoIter { 9 | pub(super) inner: hash_map::IntoIter, 10 | } 11 | 12 | impl Iterator for IntoIter { 13 | type Item = (HeaderName, HeaderValues); 14 | 15 | fn next(&mut self) -> Option { 16 | self.inner.next() 17 | } 18 | 19 | #[inline] 20 | fn size_hint(&self) -> (usize, Option) { 21 | self.inner.size_hint() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/headers/iter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::iter::Iterator; 3 | 4 | use crate::headers::{HeaderName, HeaderValues}; 5 | 6 | /// Iterator over the headers. 7 | #[derive(Debug)] 8 | pub struct Iter<'a> { 9 | pub(super) inner: hash_map::Iter<'a, HeaderName, HeaderValues>, 10 | } 11 | 12 | impl<'a> Iterator for Iter<'a> { 13 | type Item = (&'a HeaderName, &'a HeaderValues); 14 | 15 | fn next(&mut self) -> Option { 16 | self.inner.next() 17 | } 18 | 19 | #[inline] 20 | fn size_hint(&self) -> (usize, Option) { 21 | self.inner.size_hint() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/headers/iter_mut.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::iter::Iterator; 3 | 4 | use crate::headers::{HeaderName, HeaderValues}; 5 | 6 | /// Iterator over the headers. 7 | #[derive(Debug)] 8 | pub struct IterMut<'a> { 9 | pub(super) inner: hash_map::IterMut<'a, HeaderName, HeaderValues>, 10 | } 11 | 12 | impl<'a> Iterator for IterMut<'a> { 13 | type Item = (&'a HeaderName, &'a mut HeaderValues); 14 | 15 | fn next(&mut self) -> Option { 16 | self.inner.next() 17 | } 18 | 19 | #[inline] 20 | fn size_hint(&self) -> (usize, Option) { 21 | self.inner.size_hint() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/headers/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP headers. 2 | 3 | mod constants; 4 | mod header; 5 | mod header_name; 6 | mod header_value; 7 | mod header_values; 8 | #[allow(clippy::module_inception)] 9 | mod headers; 10 | mod into_iter; 11 | mod iter; 12 | mod iter_mut; 13 | mod names; 14 | mod to_header_values; 15 | mod values; 16 | 17 | pub use constants::*; 18 | pub use header::Header; 19 | pub use header_name::HeaderName; 20 | pub use header_value::HeaderValue; 21 | pub use header_values::HeaderValues; 22 | pub use headers::Headers; 23 | pub use into_iter::IntoIter; 24 | pub use iter::Iter; 25 | pub use iter_mut::IterMut; 26 | pub use names::Names; 27 | pub use to_header_values::ToHeaderValues; 28 | pub use values::Values; 29 | -------------------------------------------------------------------------------- /src/headers/names.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::iter::Iterator; 3 | 4 | use crate::headers::{HeaderName, HeaderValues}; 5 | 6 | /// Iterator over the headers. 7 | #[derive(Debug)] 8 | pub struct Names<'a> { 9 | pub(super) inner: hash_map::Keys<'a, HeaderName, HeaderValues>, 10 | } 11 | 12 | impl<'a> Iterator for Names<'a> { 13 | type Item = &'a HeaderName; 14 | 15 | fn next(&mut self) -> Option { 16 | self.inner.next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/headers/to_header_values.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io; 3 | use std::iter; 4 | use std::option; 5 | use std::slice; 6 | 7 | use crate::headers::{Header, HeaderValue, HeaderValues, Values}; 8 | 9 | /// A trait for objects which can be converted or resolved to one or more `HeaderValue`s. 10 | pub trait ToHeaderValues { 11 | /// Returned iterator over header values which this type may correspond to. 12 | type Iter: Iterator; 13 | 14 | /// Converts this object to an iterator of resolved `HeaderValues`. 15 | fn to_header_values(&self) -> crate::Result; 16 | } 17 | 18 | impl ToHeaderValues for T { 19 | type Iter = option::IntoIter; 20 | 21 | fn to_header_values(&self) -> crate::Result { 22 | Ok(Some(self.header_value()).into_iter()) 23 | } 24 | } 25 | 26 | impl ToHeaderValues for HeaderValue { 27 | type Iter = option::IntoIter; 28 | 29 | fn to_header_values(&self) -> crate::Result { 30 | Ok(Some(self.clone()).into_iter()) 31 | } 32 | } 33 | 34 | impl<'a> ToHeaderValues for &'a HeaderValues { 35 | type Iter = iter::Cloned>; 36 | 37 | fn to_header_values(&self) -> crate::Result { 38 | Ok(self.iter().cloned()) 39 | } 40 | } 41 | 42 | impl<'a> ToHeaderValues for &'a [HeaderValue] { 43 | type Iter = iter::Cloned>; 44 | 45 | fn to_header_values(&self) -> crate::Result { 46 | Ok(self.iter().cloned()) 47 | } 48 | } 49 | 50 | impl<'a> ToHeaderValues for &'a str { 51 | type Iter = option::IntoIter; 52 | 53 | fn to_header_values(&self) -> crate::Result { 54 | let value = self 55 | .parse() 56 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; 57 | Ok(Some(value).into_iter()) 58 | } 59 | } 60 | 61 | impl ToHeaderValues for String { 62 | type Iter = option::IntoIter; 63 | 64 | fn to_header_values(&self) -> crate::Result { 65 | self.as_str().to_header_values() 66 | } 67 | } 68 | 69 | impl ToHeaderValues for &String { 70 | type Iter = option::IntoIter; 71 | 72 | fn to_header_values(&self) -> crate::Result { 73 | self.as_str().to_header_values() 74 | } 75 | } 76 | 77 | impl ToHeaderValues for Cow<'_, str> { 78 | type Iter = option::IntoIter; 79 | 80 | fn to_header_values(&self) -> crate::Result { 81 | self.as_ref().to_header_values() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/headers/values.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::iter::Iterator; 3 | 4 | use crate::headers::{HeaderName, HeaderValue, HeaderValues}; 5 | 6 | /// Iterator over the header values. 7 | #[derive(Debug)] 8 | pub struct Values<'a> { 9 | pub(super) inner: Option>, 10 | slot: Option<&'a HeaderValues>, 11 | cursor: usize, 12 | } 13 | 14 | impl<'a> Values<'a> { 15 | /// Constructor for `Headers`. 16 | pub(crate) fn new(inner: hash_map::Values<'a, HeaderName, HeaderValues>) -> Self { 17 | Self { 18 | inner: Some(inner), 19 | slot: None, 20 | cursor: 0, 21 | } 22 | } 23 | 24 | /// Constructor for `HeaderValues`. 25 | pub(crate) fn new_values(values: &'a HeaderValues) -> Self { 26 | Self { 27 | inner: None, 28 | slot: Some(values), 29 | cursor: 0, 30 | } 31 | } 32 | } 33 | 34 | impl<'a> Iterator for Values<'a> { 35 | type Item = &'a HeaderValue; 36 | 37 | fn next(&mut self) -> Option { 38 | loop { 39 | // Check if we have a vec in the current slot, and if not set one. 40 | if self.slot.is_none() { 41 | let next = match self.inner.as_mut() { 42 | Some(inner) => inner.next()?, 43 | None => return None, 44 | }; 45 | self.cursor = 0; 46 | self.slot = Some(next); 47 | } 48 | 49 | // Get the next item 50 | match self.slot.unwrap().get(self.cursor) { 51 | // If an item is found, increment the cursor and return the item. 52 | Some(item) => { 53 | self.cursor += 1; 54 | return Some(item); 55 | } 56 | // If no item is found, unset the slot and loop again. 57 | None => { 58 | self.slot = None; 59 | continue; 60 | } 61 | } 62 | } 63 | } 64 | 65 | #[inline] 66 | fn size_hint(&self) -> (usize, Option) { 67 | match self.inner.as_ref() { 68 | Some(inner) => inner.size_hint(), 69 | None => (0, None), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/mime/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::mime::Mime; 2 | use std::borrow::Cow; 3 | 4 | macro_rules! utf8_mime_const { 5 | ($name:ident, $desc:expr, $base:expr, $sub:expr) => { 6 | mime_const!( 7 | with_params, 8 | $name, 9 | $desc, 10 | $base, 11 | $sub, 12 | true, 13 | ";charset=utf-8" 14 | ); 15 | }; 16 | } 17 | macro_rules! mime_const { 18 | ($name:ident, $desc:expr, $base:expr, $sub:expr) => { 19 | mime_const!(with_params, $name, $desc, $base, $sub, false, ""); 20 | }; 21 | 22 | (with_params, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { 23 | mime_const!( 24 | doc_expanded, 25 | $name, 26 | $desc, 27 | $base, 28 | $sub, 29 | $is_utf8, 30 | concat!( 31 | "Content-Type for ", 32 | $desc, 33 | ".\n\n# Mime Type\n\n```text\n", 34 | $base, 35 | "/", 36 | $sub, 37 | $doccomment, 38 | "\n```" 39 | ) 40 | ); 41 | }; 42 | 43 | (doc_expanded, $name:ident, $desc:expr, $base:expr, $sub:expr, $is_utf8:expr, $doccomment:expr) => { 44 | #[doc = $doccomment] 45 | pub const $name: Mime = Mime { 46 | essence: Cow::Borrowed(concat!($base, "/", $sub)), 47 | basetype: Cow::Borrowed($base), 48 | subtype: Cow::Borrowed($sub), 49 | is_utf8: $is_utf8, 50 | params: vec![], 51 | }; 52 | }; 53 | } 54 | 55 | utf8_mime_const!(JAVASCRIPT, "JavaScript", "text", "javascript"); 56 | utf8_mime_const!(CSS, "CSS", "text", "css"); 57 | utf8_mime_const!(HTML, "HTML", "text", "html"); 58 | utf8_mime_const!(PLAIN, "Plain text", "text", "plain"); 59 | utf8_mime_const!(XML, "XML", "application", "xml"); 60 | utf8_mime_const!(RSS, "RSS Feed", "application", "rss+xml"); 61 | utf8_mime_const!(ATOM, "Atom Feed", "application", "atom+xml"); 62 | mime_const!(ANY, "matching anything", "*", "*"); 63 | mime_const!(JSON, "JSON", "application", "json"); 64 | mime_const!(SSE, "Server Sent Events", "text", "event-stream"); 65 | mime_const!(BYTE_STREAM, "byte streams", "application", "octet-stream"); 66 | mime_const!(FORM, "forms", "application", "x-www-form-urlencoded"); 67 | mime_const!(MULTIPART_FORM, "multipart forms", "multipart", "form-data"); 68 | mime_const!(WASM, "webassembly", "application", "wasm"); 69 | 70 | // Images 71 | // https://www.iana.org/assignments/media-types/media-types.xhtml#image 72 | mime_const!(BMP, "BMP images", "image", "bmp"); 73 | mime_const!(JPEG, "JPEG images", "image", "jpeg"); 74 | mime_const!(PNG, "PNG images", "image", "png"); 75 | mime_const!(SVG, "SVG", "image", "svg+xml"); 76 | mime_const!(WEBP, "WebP images", "image", "webp"); 77 | 78 | // Audio 79 | // https://www.iana.org/assignments/media-types/media-types.xhtml#audio 80 | mime_const!(MIDI, "MIDI audio", "audio", "midi"); 81 | mime_const!(MP3, "MPEG audio layer 3", "audio", "mpeg"); 82 | mime_const!(OGG, "Ogg vorbis audio", "audio", "ogg"); 83 | mime_const!(OPUS, "Opus audio", "audio", "opus"); 84 | mime_const!(M4A, "MPEG audio layer 4", "audio", "mp4"); 85 | 86 | // Video 87 | // https://www.iana.org/assignments/media-types/media-types.xhtml#video 88 | mime_const!(MP4, "MPEG video layer 4", "video", "mp4"); 89 | mime_const!(MPEG, "MPEG video", "video", "mpeg"); 90 | mime_const!(WEBM, "WebM video", "video", "webm"); 91 | mime_const!(AVI, "Microsoft AVI video", "video", "x-msvideo"); 92 | // There are multiple `.ico` mime types known, but `image/x-icon` 93 | // is what most browser use. See: 94 | // https://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type 95 | mime_const!(ICO, "ICO icons", "image", "x-icon"); 96 | 97 | // Fonts 98 | // https://www.iana.org/assignments/media-types/media-types.xhtml#font 99 | mime_const!(OTF, "OTF", "font", "otf"); 100 | mime_const!(TTF, "TTF", "font", "ttf"); 101 | mime_const!(WOFF, "WOFF", "font", "woff"); 102 | mime_const!(WOFF2, "WOFF2", "font", "woff2"); 103 | 104 | // Archives 105 | mime_const!(ZIP, "Zip archive", "application", "zip"); 106 | mime_const!(SEVENZIP, "7Zip archive", "application", "x-7z-compressed"); 107 | -------------------------------------------------------------------------------- /src/other/date.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, DATE}; 2 | use crate::utils::HttpDate; 3 | 4 | use std::time::SystemTime; 5 | 6 | /// The date and time at which the message originated. 7 | /// 8 | /// # Specifications 9 | /// 10 | /// - [RFC 7231, section 7.1.1.2: Date](https://tools.ietf.org/html/rfc7231#section-7.1.1.2) 11 | /// 12 | /// # Examples 13 | /// 14 | /// ``` 15 | /// # fn main() -> http_types::Result<()> { 16 | /// # 17 | /// use http_types::Response; 18 | /// use http_types::other::Date; 19 | /// 20 | /// use std::time::{Duration, SystemTime}; 21 | /// 22 | /// let now = SystemTime::now(); 23 | /// let date = Date::new(now); 24 | /// 25 | /// let mut res = Response::new(200); 26 | /// res.insert_header(&date, &date); 27 | /// 28 | /// let date = Date::from_headers(res)?.unwrap(); 29 | /// 30 | /// // Validate we're within 1 second accurate of the system time. 31 | /// assert!(now.duration_since(date.into())? <= Duration::from_secs(1)); 32 | /// # 33 | /// # Ok(()) } 34 | /// ``` 35 | #[derive(Debug)] 36 | pub struct Date { 37 | at: SystemTime, 38 | } 39 | 40 | impl Date { 41 | /// Create a new instance. 42 | pub fn new(at: SystemTime) -> Self { 43 | Self { at } 44 | } 45 | 46 | /// Create a new instance with the date set to now. 47 | pub fn now() -> Self { 48 | Self { 49 | at: SystemTime::now(), 50 | } 51 | } 52 | 53 | /// Create a new instance from headers. 54 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 55 | let headers = match headers.as_ref().get(DATE) { 56 | Some(headers) => headers, 57 | None => return Ok(None), 58 | }; 59 | 60 | // If we successfully parsed the header then there's always at least one 61 | // entry. We want the last entry. 62 | let value = headers.iter().last().unwrap(); 63 | let date: HttpDate = value 64 | .as_str() 65 | .trim() 66 | .parse() 67 | .map_err(|mut e: crate::Error| { 68 | e.set_status(400); 69 | e 70 | })?; 71 | let at = date.into(); 72 | Ok(Some(Self { at })) 73 | } 74 | } 75 | 76 | impl Header for Date { 77 | fn header_name(&self) -> HeaderName { 78 | DATE 79 | } 80 | 81 | fn header_value(&self) -> HeaderValue { 82 | let date: HttpDate = self.at.into(); 83 | let output = format!("{}", date); 84 | 85 | // SAFETY: the internal string is validated to be ASCII. 86 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 87 | } 88 | } 89 | 90 | impl From for SystemTime { 91 | fn from(date: Date) -> Self { 92 | date.at 93 | } 94 | } 95 | 96 | impl From for Date { 97 | fn from(time: SystemTime) -> Self { 98 | Self { at: time } 99 | } 100 | } 101 | 102 | impl PartialEq for Date { 103 | fn eq(&self, other: &SystemTime) -> bool { 104 | &self.at == other 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod test { 110 | use super::*; 111 | use crate::headers::Headers; 112 | use std::time::Duration; 113 | 114 | #[test] 115 | fn smoke() -> crate::Result<()> { 116 | let now = SystemTime::now(); 117 | let date = Date::new(now); 118 | 119 | let mut headers = Headers::new(); 120 | date.apply_header(&mut headers); 121 | 122 | let date = Date::from_headers(headers)?.unwrap(); 123 | 124 | // Validate we're within 1 second accurate of the system time. 125 | assert!(now.duration_since(date.into())? <= Duration::from_secs(1)); 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn bad_request_on_parse_error() { 131 | let mut headers = Headers::new(); 132 | headers.insert(DATE, "").unwrap(); 133 | let err = Date::from_headers(headers).unwrap_err(); 134 | assert_eq!(err.status(), 400); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/other/expect.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{HeaderName, HeaderValue, Headers, EXPECT}; 2 | use crate::{ensure_eq_status, headers::Header}; 3 | 4 | use std::fmt::Debug; 5 | 6 | /// HTTP `Expect` header 7 | /// 8 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect) 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7231, section 5.1.1: Expect](https://tools.ietf.org/html/rfc7231#section-5.1.1) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::Response; 20 | /// use http_types::other::Expect; 21 | /// 22 | /// let expect = Expect::new(); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&expect, &expect); 26 | /// 27 | /// let expect = Expect::from_headers(res)?.unwrap(); 28 | /// assert_eq!(expect, Expect::new()); 29 | /// # 30 | /// # Ok(()) } 31 | /// ``` 32 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] 33 | pub struct Expect { 34 | _priv: (), 35 | } 36 | 37 | impl Expect { 38 | /// Create a new instance of `Expect`. 39 | pub fn new() -> Self { 40 | Self { _priv: () } 41 | } 42 | 43 | /// Create an instance of `Expect` from a `Headers` instance. 44 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 45 | let headers = match headers.as_ref().get(EXPECT) { 46 | Some(headers) => headers, 47 | None => return Ok(None), 48 | }; 49 | 50 | // If we successfully parsed the header then there's always at least one 51 | // entry. We want the last entry. 52 | let header = headers.iter().last().unwrap(); 53 | ensure_eq_status!(header, "100-continue", 400, "malformed `Expect` header"); 54 | 55 | Ok(Some(Self { _priv: () })) 56 | } 57 | } 58 | 59 | impl Header for Expect { 60 | fn header_name(&self) -> HeaderName { 61 | EXPECT 62 | } 63 | fn header_value(&self) -> HeaderValue { 64 | let value = "100-continue"; 65 | // SAFETY: the internal string is validated to be ASCII. 66 | unsafe { HeaderValue::from_bytes_unchecked(value.into()) } 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod test { 72 | use super::*; 73 | use crate::headers::Headers; 74 | 75 | #[test] 76 | fn smoke() -> crate::Result<()> { 77 | let expect = Expect::new(); 78 | 79 | let mut headers = Headers::new(); 80 | expect.apply_header(&mut headers); 81 | 82 | let expect = Expect::from_headers(headers)?.unwrap(); 83 | assert_eq!(expect, Expect::new()); 84 | Ok(()) 85 | } 86 | 87 | #[test] 88 | fn bad_request_on_parse_error() { 89 | let mut headers = Headers::new(); 90 | headers.insert(EXPECT, "").unwrap(); 91 | let err = Expect::from_headers(headers).unwrap_err(); 92 | assert_eq!(err.status(), 400); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/other/mod.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous HTTP headers. 2 | 3 | mod date; 4 | mod expect; 5 | mod referer; 6 | mod retry_after; 7 | mod source_map; 8 | 9 | pub use date::Date; 10 | pub use expect::Expect; 11 | pub use referer::Referer; 12 | pub use retry_after::RetryAfter; 13 | pub use source_map::SourceMap; 14 | -------------------------------------------------------------------------------- /src/other/referer.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, REFERER}; 2 | use crate::{bail_status as bail, Status, Url}; 3 | 4 | use std::convert::TryInto; 5 | 6 | /// Contains the address of the page making the request. 7 | /// 8 | /// __Important__: Although this header has many innocent uses it can have 9 | /// undesirable consequences for user security and privacy. 10 | /// 11 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer) 12 | /// 13 | /// # Specifications 14 | /// 15 | /// - [RFC 7231, section 5.5.2: Referer](https://tools.ietf.org/html/rfc7231#section-5.5.2) 16 | /// 17 | /// # Examples 18 | /// 19 | /// ``` 20 | /// # fn main() -> http_types::Result<()> { 21 | /// # 22 | /// use http_types::{Response, Url}; 23 | /// use http_types::other::Referer; 24 | /// 25 | /// let referer = Referer::new(Url::parse("https://example.net/")?); 26 | /// 27 | /// let mut res = Response::new(200); 28 | /// res.insert_header(&referer, &referer); 29 | /// 30 | /// let base_url = Url::parse("https://example.net/")?; 31 | /// let referer = Referer::from_headers(base_url, res)?.unwrap(); 32 | /// assert_eq!(referer.location(), &Url::parse("https://example.net/")?); 33 | /// # 34 | /// # Ok(()) } 35 | /// ``` 36 | #[derive(Debug)] 37 | pub struct Referer { 38 | location: Url, 39 | } 40 | 41 | impl Referer { 42 | /// Create a new instance of `Referer` header. 43 | pub fn new(location: Url) -> Self { 44 | Self { location } 45 | } 46 | 47 | /// Create a new instance from headers. 48 | pub fn from_headers(base_url: U, headers: impl AsRef) -> crate::Result> 49 | where 50 | U: TryInto, 51 | U::Error: std::fmt::Debug, 52 | { 53 | let headers = match headers.as_ref().get(REFERER) { 54 | Some(headers) => headers, 55 | None => return Ok(None), 56 | }; 57 | 58 | // If we successfully parsed the header then there's always at least one 59 | // entry. We want the last entry. 60 | let header_value = headers.iter().last().unwrap(); 61 | 62 | let url = match Url::parse(header_value.as_str()) { 63 | Ok(url) => url, 64 | Err(_) => match base_url.try_into() { 65 | Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, 66 | Err(_) => bail!(500, "Invalid base url provided"), 67 | }, 68 | }; 69 | 70 | Ok(Some(Self { location: url })) 71 | } 72 | 73 | /// Get the url. 74 | pub fn location(&self) -> &Url { 75 | &self.location 76 | } 77 | 78 | /// Set the url. 79 | pub fn set_location(&mut self, location: U) -> Result<(), U::Error> 80 | where 81 | U: TryInto, 82 | U::Error: std::fmt::Debug, 83 | { 84 | self.location = location.try_into()?; 85 | Ok(()) 86 | } 87 | } 88 | 89 | impl Header for Referer { 90 | fn header_name(&self) -> HeaderName { 91 | REFERER 92 | } 93 | 94 | fn header_value(&self) -> HeaderValue { 95 | let output = self.location.to_string(); 96 | 97 | // SAFETY: the internal string is validated to be ASCII. 98 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod test { 104 | use super::*; 105 | use crate::headers::Headers; 106 | 107 | #[test] 108 | fn smoke() -> crate::Result<()> { 109 | let referer = Referer::new(Url::parse("https://example.net/test.json")?); 110 | 111 | let mut headers = Headers::new(); 112 | referer.apply_header(&mut headers); 113 | 114 | let base_url = Url::parse("https://example.net/")?; 115 | let referer = Referer::from_headers(base_url, headers)?.unwrap(); 116 | assert_eq!( 117 | referer.location(), 118 | &Url::parse("https://example.net/test.json")? 119 | ); 120 | Ok(()) 121 | } 122 | 123 | #[test] 124 | fn bad_request_on_parse_error() { 125 | let mut headers = Headers::new(); 126 | headers 127 | .insert(REFERER, "htt://") 128 | .unwrap(); 129 | let err = 130 | Referer::from_headers(Url::parse("https://example.net").unwrap(), headers).unwrap_err(); 131 | assert_eq!(err.status(), 500); 132 | } 133 | 134 | #[test] 135 | fn fallback_works() -> crate::Result<()> { 136 | let mut headers = Headers::new(); 137 | headers.insert(REFERER, "/test.json").unwrap(); 138 | 139 | let base_url = Url::parse("https://fallback.net/")?; 140 | let referer = Referer::from_headers(base_url, headers)?.unwrap(); 141 | assert_eq!( 142 | referer.location(), 143 | &Url::parse("https://fallback.net/test.json")? 144 | ); 145 | 146 | let mut headers = Headers::new(); 147 | headers 148 | .insert(REFERER, "https://example.com/test.json") 149 | .unwrap(); 150 | 151 | let base_url = Url::parse("https://fallback.net/")?; 152 | let referer = Referer::from_headers(base_url, headers)?.unwrap(); 153 | assert_eq!( 154 | referer.location(), 155 | &Url::parse("https://example.com/test.json")? 156 | ); 157 | Ok(()) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/other/retry_after.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime, SystemTimeError}; 2 | 3 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, RETRY_AFTER}; 4 | use crate::utils::{fmt_http_date, parse_http_date}; 5 | 6 | /// Indicate how long the user agent should wait before making a follow-up request. 7 | /// 8 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```no_run 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::other::RetryAfter; 20 | /// use http_types::Response; 21 | /// use std::time::{SystemTime, Duration}; 22 | /// use async_std::task; 23 | /// 24 | /// let retry = RetryAfter::new(Duration::from_secs(10)); 25 | /// 26 | /// let mut headers = Response::new(429); 27 | /// headers.insert_header(&retry, &retry); 28 | /// 29 | /// // Sleep for the duration, then try the task again. 30 | /// let retry = RetryAfter::from_headers(headers)?.unwrap(); 31 | /// task::sleep(retry.duration_since(SystemTime::now())?); 32 | /// # 33 | /// # Ok(()) } 34 | /// ``` 35 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 36 | pub struct RetryAfter { 37 | inner: RetryDirective, 38 | } 39 | 40 | #[allow(clippy::len_without_is_empty)] 41 | impl RetryAfter { 42 | /// Create a new instance from a `Duration`. 43 | /// 44 | /// This value will be encoded over the wire as a relative offset in seconds. 45 | pub fn new(dur: Duration) -> Self { 46 | Self { 47 | inner: RetryDirective::Duration(dur), 48 | } 49 | } 50 | 51 | /// Create a new instance from a `SystemTime` instant. 52 | /// 53 | /// This value will be encoded a specific `Date` over the wire. 54 | pub fn new_at(at: SystemTime) -> Self { 55 | Self { 56 | inner: RetryDirective::SystemTime(at), 57 | } 58 | } 59 | 60 | /// Create a new instance from headers. 61 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 62 | let header = match headers.as_ref().get(RETRY_AFTER) { 63 | Some(headers) => headers.last(), 64 | None => return Ok(None), 65 | }; 66 | 67 | let inner = match header.as_str().parse::() { 68 | Ok(dur) => RetryDirective::Duration(Duration::from_secs(dur)), 69 | Err(_) => { 70 | let at = parse_http_date(header.as_str())?; 71 | RetryDirective::SystemTime(at) 72 | } 73 | }; 74 | Ok(Some(Self { inner })) 75 | } 76 | 77 | /// Returns the amount of time elapsed from an earlier point in time. 78 | /// 79 | /// # Errors 80 | /// 81 | /// This may return an error if the `earlier` time was after the current time. 82 | pub fn duration_since(&self, earlier: SystemTime) -> Result { 83 | let at = match self.inner { 84 | RetryDirective::Duration(dur) => SystemTime::now() + dur, 85 | RetryDirective::SystemTime(at) => at, 86 | }; 87 | 88 | at.duration_since(earlier) 89 | } 90 | } 91 | 92 | impl Header for RetryAfter { 93 | fn header_name(&self) -> HeaderName { 94 | RETRY_AFTER 95 | } 96 | 97 | fn header_value(&self) -> HeaderValue { 98 | let output = match self.inner { 99 | RetryDirective::Duration(dur) => format!("{}", dur.as_secs()), 100 | RetryDirective::SystemTime(at) => fmt_http_date(at), 101 | }; 102 | // SAFETY: the internal string is validated to be ASCII. 103 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 104 | } 105 | } 106 | 107 | impl From for SystemTime { 108 | fn from(retry_after: RetryAfter) -> Self { 109 | match retry_after.inner { 110 | RetryDirective::Duration(dur) => SystemTime::now() + dur, 111 | RetryDirective::SystemTime(at) => at, 112 | } 113 | } 114 | } 115 | 116 | /// What value are we decoding into? 117 | /// 118 | /// This value is intionally never exposes; all end-users want is a `Duration` 119 | /// value that tells them how long to wait for before trying again. 120 | #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 121 | enum RetryDirective { 122 | Duration(Duration), 123 | SystemTime(SystemTime), 124 | } 125 | 126 | #[cfg(test)] 127 | mod test { 128 | use super::*; 129 | use crate::headers::Headers; 130 | 131 | #[test] 132 | fn smoke() -> crate::Result<()> { 133 | let retry = RetryAfter::new(Duration::from_secs(10)); 134 | 135 | let mut headers = Headers::new(); 136 | retry.apply_header(&mut headers); 137 | 138 | // `SystemTime::now` uses sub-second precision which means there's some 139 | // offset that's not encoded. 140 | let now = SystemTime::now(); 141 | let retry = RetryAfter::from_headers(headers)?.unwrap(); 142 | assert_eq!( 143 | retry.duration_since(now)?.as_secs(), 144 | Duration::from_secs(10).as_secs() 145 | ); 146 | Ok(()) 147 | } 148 | 149 | #[test] 150 | fn new_at() -> crate::Result<()> { 151 | let now = SystemTime::now(); 152 | let retry = RetryAfter::new_at(now + Duration::from_secs(10)); 153 | 154 | let mut headers = Headers::new(); 155 | retry.apply_header(&mut headers); 156 | 157 | // `SystemTime::now` uses sub-second precision which means there's some 158 | // offset that's not encoded. 159 | let retry = RetryAfter::from_headers(headers)?.unwrap(); 160 | let delta = retry.duration_since(now)?; 161 | assert!(delta >= Duration::from_secs(9)); 162 | assert!(delta <= Duration::from_secs(10)); 163 | Ok(()) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/other/source_map.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, SOURCE_MAP}; 2 | use crate::{bail_status as bail, Status, Url}; 3 | 4 | use std::convert::TryInto; 5 | 6 | /// Links to a file that maps transformed source to the original source. 7 | /// 8 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap) 9 | /// 10 | /// # Specifications 11 | /// 12 | /// - [Source Map Revision 3](https://sourcemaps.info/spec.html) 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// # fn main() -> http_types::Result<()> { 18 | /// # 19 | /// use http_types::{Response, Url}; 20 | /// use http_types::other::SourceMap; 21 | /// 22 | /// let source_map = SourceMap::new(Url::parse("https://example.net/")?); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&source_map, &source_map); 26 | /// 27 | /// let base_url = Url::parse("https://example.net/")?; 28 | /// let source_map = SourceMap::from_headers(base_url, res)?.unwrap(); 29 | /// assert_eq!(source_map.location(), &Url::parse("https://example.net/")?); 30 | /// # 31 | /// # Ok(()) } 32 | /// ``` 33 | #[derive(Debug)] 34 | pub struct SourceMap { 35 | location: Url, 36 | } 37 | 38 | impl SourceMap { 39 | /// Create a new instance of `SourceMap` header. 40 | pub fn new(location: Url) -> Self { 41 | Self { location } 42 | } 43 | 44 | /// Create a new instance from headers. 45 | pub fn from_headers(base_url: U, headers: impl AsRef) -> crate::Result> 46 | where 47 | U: TryInto, 48 | U::Error: std::fmt::Debug, 49 | { 50 | let headers = match headers.as_ref().get(SOURCE_MAP) { 51 | Some(headers) => headers, 52 | None => return Ok(None), 53 | }; 54 | 55 | // If we successfully parsed the header then there's always at least one 56 | // entry. We want the last entry. 57 | let header_value = headers.iter().last().unwrap(); 58 | 59 | let url = match Url::parse(header_value.as_str()) { 60 | Ok(url) => url, 61 | Err(_) => match base_url.try_into() { 62 | Ok(base_url) => base_url.join(header_value.as_str().trim()).status(500)?, 63 | Err(_) => bail!(500, "Invalid base url provided"), 64 | }, 65 | }; 66 | 67 | Ok(Some(Self { location: url })) 68 | } 69 | 70 | /// Get the url. 71 | pub fn location(&self) -> &Url { 72 | &self.location 73 | } 74 | 75 | /// Set the url. 76 | pub fn set_location(&mut self, location: U) -> Result<(), U::Error> 77 | where 78 | U: TryInto, 79 | U::Error: std::fmt::Debug, 80 | { 81 | self.location = location.try_into()?; 82 | Ok(()) 83 | } 84 | } 85 | 86 | impl Header for SourceMap { 87 | fn header_name(&self) -> HeaderName { 88 | SOURCE_MAP 89 | } 90 | 91 | fn header_value(&self) -> HeaderValue { 92 | let output = self.location.to_string(); 93 | 94 | // SAFETY: the internal string is validated to be ASCII. 95 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::*; 102 | use crate::headers::Headers; 103 | 104 | #[test] 105 | fn smoke() -> crate::Result<()> { 106 | let source_map = SourceMap::new(Url::parse("https://example.net/test.json")?); 107 | 108 | let mut headers = Headers::new(); 109 | source_map.apply_header(&mut headers); 110 | 111 | let base_url = Url::parse("https://example.net/")?; 112 | let source_map = SourceMap::from_headers(base_url, headers)?.unwrap(); 113 | assert_eq!( 114 | source_map.location(), 115 | &Url::parse("https://example.net/test.json")? 116 | ); 117 | Ok(()) 118 | } 119 | 120 | #[test] 121 | fn bad_request_on_parse_error() { 122 | let mut headers = Headers::new(); 123 | headers 124 | .insert(SOURCE_MAP, "htt://") 125 | .unwrap(); 126 | let err = SourceMap::from_headers(Url::parse("https://example.net").unwrap(), headers) 127 | .unwrap_err(); 128 | assert_eq!(err.status(), 500); 129 | } 130 | 131 | #[test] 132 | fn fallback_works() -> crate::Result<()> { 133 | let mut headers = Headers::new(); 134 | headers.insert(SOURCE_MAP, "/test.json").unwrap(); 135 | 136 | let base_url = Url::parse("https://fallback.net/")?; 137 | let source_map = SourceMap::from_headers(base_url, headers)?.unwrap(); 138 | assert_eq!( 139 | source_map.location(), 140 | &Url::parse("https://fallback.net/test.json")? 141 | ); 142 | 143 | let mut headers = Headers::new(); 144 | headers 145 | .insert(SOURCE_MAP, "https://example.com/test.json") 146 | .unwrap(); 147 | 148 | let base_url = Url::parse("https://fallback.net/")?; 149 | let source_map = SourceMap::from_headers(base_url, headers)?.unwrap(); 150 | assert_eq!( 151 | source_map.location(), 152 | &Url::parse("https://example.com/test.json")? 153 | ); 154 | Ok(()) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/parse_utils.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | /// https://tools.ietf.org/html/rfc7230#section-3.2.6 4 | pub(crate) fn parse_token(input: &str) -> (Option<&str>, &str) { 5 | let mut end_of_token = 0; 6 | for (i, c) in input.char_indices() { 7 | if tchar(c) { 8 | end_of_token = i + 1; 9 | } else { 10 | break; 11 | } 12 | } 13 | 14 | if end_of_token == 0 { 15 | (None, input) 16 | } else { 17 | (Some(&input[..end_of_token]), &input[end_of_token..]) 18 | } 19 | } 20 | 21 | /// https://tools.ietf.org/html/rfc7230#section-3.2.6 22 | fn tchar(c: char) -> bool { 23 | matches!( 24 | c, 'a'..='z' 25 | | 'A'..='Z' 26 | | '0'..='9' 27 | | '!' 28 | | '#' 29 | | '$' 30 | | '%' 31 | | '&' 32 | | '\'' 33 | | '*' 34 | | '+' 35 | | '-' 36 | | '.' 37 | | '^' 38 | | '_' 39 | | '`' 40 | | '|' 41 | | '~' 42 | ) 43 | } 44 | 45 | /// https://tools.ietf.org/html/rfc7230#section-3.2.6 46 | fn vchar(c: char) -> bool { 47 | matches!(c as u8, b'\t' | 32..=126 | 128..=255) 48 | } 49 | 50 | /// https://tools.ietf.org/html/rfc7230#section-3.2.6 51 | pub(crate) fn parse_quoted_string(input: &str) -> (Option>, &str) { 52 | // quoted-string must start with a DQUOTE 53 | if !input.starts_with('"') { 54 | return (None, input); 55 | } 56 | 57 | let mut end_of_string = None; 58 | let mut backslashes: Vec = vec![]; 59 | 60 | for (i, c) in input.char_indices().skip(1) { 61 | if i > 1 && backslashes.last() == Some(&(i - 2)) { 62 | if !vchar(c) { 63 | // only VCHARs can be escaped 64 | return (None, input); 65 | } 66 | // otherwise, we skip over this character while parsing 67 | } else { 68 | match c as u8 { 69 | // we have reached a quoted-pair 70 | b'\\' => { 71 | backslashes.push(i - 1); 72 | } 73 | 74 | // end of the string, DQUOTE 75 | b'"' => { 76 | end_of_string = Some(i + 1); 77 | break; 78 | } 79 | 80 | // qdtext 81 | b'\t' | b' ' | 15 | 35..=91 | 93..=126 | 128..=255 => {} 82 | 83 | // unexpected character, bail 84 | _ => return (None, input), 85 | } 86 | } 87 | } 88 | 89 | if let Some(end_of_string) = end_of_string { 90 | let value = &input[1..end_of_string - 1]; // strip DQUOTEs from start and end 91 | 92 | let value = if backslashes.is_empty() { 93 | // no backslashes means we don't need to allocate 94 | value.into() 95 | } else { 96 | backslashes.reverse(); // so that we can use pop. goes from low-to-high to high-to-low sorting 97 | 98 | value 99 | .char_indices() 100 | .filter_map(|(i, c)| { 101 | if Some(&i) == backslashes.last() { 102 | // they're already sorted highest to lowest, so we only need to check the last one 103 | backslashes.pop(); 104 | None // remove the backslash from the output 105 | } else { 106 | Some(c) 107 | } 108 | }) 109 | .collect::() 110 | .into() 111 | }; 112 | 113 | (Some(value), &input[end_of_string..]) 114 | } else { 115 | // we never reached a closing DQUOTE, so we do not have a valid quoted-string 116 | (None, input) 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod test { 122 | use super::*; 123 | #[test] 124 | fn token_successful_parses() { 125 | assert_eq!(parse_token("key=value"), (Some("key"), "=value")); 126 | assert_eq!(parse_token("KEY=value"), (Some("KEY"), "=value")); 127 | assert_eq!(parse_token("0123)=value"), (Some("0123"), ")=value")); 128 | assert_eq!(parse_token("a=b"), (Some("a"), "=b")); 129 | assert_eq!( 130 | parse_token("!#$%&'*+-.^_`|~=value"), 131 | (Some("!#$%&'*+-.^_`|~"), "=value",) 132 | ); 133 | } 134 | 135 | #[test] 136 | fn token_unsuccessful_parses() { 137 | assert_eq!(parse_token(""), (None, "")); 138 | assert_eq!(parse_token("=value"), (None, "=value")); 139 | for c in r#"(),/:;<=>?@[\]{}"#.chars() { 140 | let s = c.to_string(); 141 | assert_eq!(parse_token(&s), (None, &*s)); 142 | 143 | let s = format!("match{}rest", s); 144 | assert_eq!(parse_token(&s), (Some("match"), &*format!("{}rest", c))); 145 | } 146 | } 147 | 148 | #[test] 149 | fn qstring_successful_parses() { 150 | assert_eq!( 151 | parse_quoted_string(r#""key"=value"#), 152 | (Some(Cow::Borrowed("key")), "=value") 153 | ); 154 | 155 | assert_eq!( 156 | parse_quoted_string(r#""escaped \" quote \""rest"#), 157 | ( 158 | Some(Cow::Owned(String::from(r#"escaped " quote ""#))), 159 | r#"rest"# 160 | ) 161 | ); 162 | } 163 | 164 | #[test] 165 | fn qstring_unsuccessful_parses() { 166 | assert_eq!(parse_quoted_string(r#""abc"#), (None, "\"abc")); 167 | assert_eq!(parse_quoted_string(r#"hello""#), (None, "hello\"",)); 168 | assert_eq!(parse_quoted_string(r#"=value\"#), (None, "=value\\")); 169 | assert_eq!(parse_quoted_string(r#"\""#), (None, r#"\""#)); 170 | assert_eq!(parse_quoted_string(r#""\""#), (None, r#""\""#)); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/proxies/mod.rs: -------------------------------------------------------------------------------- 1 | //! Headers that are set by proxies 2 | mod forwarded; 3 | pub use forwarded::Forwarded; 4 | -------------------------------------------------------------------------------- /src/server/allow.rs: -------------------------------------------------------------------------------- 1 | //! List the set of methods supported by a resource. 2 | 3 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, ALLOW}; 4 | use crate::Method; 5 | 6 | use std::collections::{hash_set, HashSet}; 7 | use std::fmt::{self, Debug, Write}; 8 | use std::iter::Iterator; 9 | 10 | use std::str::FromStr; 11 | 12 | /// List the set of methods supported by a resource. 13 | /// 14 | /// # Specifications 15 | /// 16 | /// - [RFC 7231, section 7.4.1: Allow](https://tools.ietf.org/html/rfc7231#section-7.4.1) 17 | /// 18 | /// # Examples 19 | /// 20 | /// ``` 21 | /// # fn main() -> http_types::Result<()> { 22 | /// # 23 | /// use http_types::{Method, Response}; 24 | /// use http_types::server::Allow; 25 | /// 26 | /// let mut allow = Allow::new(); 27 | /// allow.insert(Method::Put); 28 | /// allow.insert(Method::Post); 29 | /// 30 | /// let mut res = Response::new(200); 31 | /// res.insert_header(&allow, &allow); 32 | /// 33 | /// let allow = Allow::from_headers(res)?.unwrap(); 34 | /// assert!(allow.contains(Method::Put)); 35 | /// assert!(allow.contains(Method::Post)); 36 | /// # 37 | /// # Ok(()) } 38 | /// ``` 39 | pub struct Allow { 40 | entries: HashSet, 41 | } 42 | 43 | impl Allow { 44 | /// Create a new instance of `Allow`. 45 | pub fn new() -> Self { 46 | Self { 47 | entries: HashSet::new(), 48 | } 49 | } 50 | 51 | /// Create a new instance from headers. 52 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 53 | let mut entries = HashSet::new(); 54 | let headers = match headers.as_ref().get(ALLOW) { 55 | Some(headers) => headers, 56 | None => return Ok(None), 57 | }; 58 | 59 | for value in headers { 60 | for part in value.as_str().trim().split(',') { 61 | let method = Method::from_str(part.trim())?; 62 | entries.insert(method); 63 | } 64 | } 65 | 66 | Ok(Some(Self { entries })) 67 | } 68 | 69 | /// Push a method into the set of methods. 70 | pub fn insert(&mut self, method: Method) { 71 | self.entries.insert(method); 72 | } 73 | 74 | /// An iterator visiting all server entries. 75 | pub fn iter(&self) -> Iter<'_> { 76 | Iter { 77 | inner: self.entries.iter(), 78 | } 79 | } 80 | 81 | /// Returns `true` if the header contains the `Method`. 82 | pub fn contains(&self, method: Method) -> bool { 83 | self.entries.contains(&method) 84 | } 85 | } 86 | 87 | impl Header for Allow { 88 | fn header_name(&self) -> HeaderName { 89 | ALLOW 90 | } 91 | fn header_value(&self) -> HeaderValue { 92 | let mut output = String::new(); 93 | for (n, method) in self.entries.iter().enumerate() { 94 | match n { 95 | 0 => write!(output, "{}", method).unwrap(), 96 | _ => write!(output, ", {}", method).unwrap(), 97 | }; 98 | } 99 | 100 | // SAFETY: the internal string is validated to be ASCII. 101 | unsafe { HeaderValue::from_bytes_unchecked(output.into()) } 102 | } 103 | } 104 | 105 | impl IntoIterator for Allow { 106 | type Item = Method; 107 | type IntoIter = IntoIter; 108 | 109 | #[inline] 110 | fn into_iter(self) -> Self::IntoIter { 111 | IntoIter { 112 | inner: self.entries.into_iter(), 113 | } 114 | } 115 | } 116 | 117 | impl<'a> IntoIterator for &'a Allow { 118 | type Item = &'a Method; 119 | type IntoIter = Iter<'a>; 120 | 121 | #[inline] 122 | fn into_iter(self) -> Self::IntoIter { 123 | self.iter() 124 | } 125 | } 126 | 127 | /// A borrowing iterator over entries in `Allow`. 128 | #[derive(Debug)] 129 | pub struct IntoIter { 130 | inner: hash_set::IntoIter, 131 | } 132 | 133 | impl Iterator for IntoIter { 134 | type Item = Method; 135 | 136 | fn next(&mut self) -> Option { 137 | self.inner.next() 138 | } 139 | 140 | #[inline] 141 | fn size_hint(&self) -> (usize, Option) { 142 | self.inner.size_hint() 143 | } 144 | } 145 | 146 | /// A lending iterator over entries in `Allow`. 147 | #[derive(Debug)] 148 | pub struct Iter<'a> { 149 | inner: hash_set::Iter<'a, Method>, 150 | } 151 | 152 | impl<'a> Iterator for Iter<'a> { 153 | type Item = &'a Method; 154 | 155 | fn next(&mut self) -> Option { 156 | self.inner.next() 157 | } 158 | 159 | #[inline] 160 | fn size_hint(&self) -> (usize, Option) { 161 | self.inner.size_hint() 162 | } 163 | } 164 | 165 | impl Debug for Allow { 166 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 167 | let mut list = f.debug_list(); 168 | for method in &self.entries { 169 | list.entry(method); 170 | } 171 | list.finish() 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod test { 177 | use super::*; 178 | use crate::headers::Headers; 179 | 180 | #[test] 181 | fn smoke() -> crate::Result<()> { 182 | let mut allow = Allow::new(); 183 | allow.insert(Method::Put); 184 | allow.insert(Method::Post); 185 | 186 | let mut headers = Headers::new(); 187 | allow.apply_header(&mut headers); 188 | 189 | let allow = Allow::from_headers(headers)?.unwrap(); 190 | assert!(allow.contains(Method::Put)); 191 | assert!(allow.contains(Method::Post)); 192 | Ok(()) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Server Context headers. 2 | 3 | pub mod allow; 4 | 5 | #[doc(inline)] 6 | pub use allow::Allow; 7 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, StatusCode}; 2 | use core::convert::{Infallible, TryInto}; 3 | use std::error::Error as StdError; 4 | use std::fmt::Debug; 5 | 6 | /// Provides the `status` method for `Result` and `Option`. 7 | /// 8 | /// This trait is sealed and cannot be implemented outside of `http-types`. 9 | pub trait Status: private::Sealed { 10 | /// Wrap the error value with an additional status code. 11 | fn status(self, status: S) -> Result 12 | where 13 | S: TryInto, 14 | S::Error: Debug; 15 | 16 | /// Wrap the error value with an additional status code that is evaluated 17 | /// lazily only once an error does occur. 18 | fn with_status(self, f: F) -> Result 19 | where 20 | S: TryInto, 21 | S::Error: Debug, 22 | F: FnOnce() -> S; 23 | } 24 | 25 | impl Status for Result 26 | where 27 | E: StdError + Send + Sync + 'static, 28 | { 29 | /// Wrap the error value with an additional status code. 30 | /// 31 | /// # Panics 32 | /// 33 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 34 | /// 35 | /// [status]: crate::Status 36 | /// [statuscode]: crate::StatusCode 37 | fn status(self, status: S) -> Result 38 | where 39 | S: TryInto, 40 | S::Error: Debug, 41 | { 42 | self.map_err(|error| { 43 | let status = status 44 | .try_into() 45 | .expect("Could not convert into a valid `StatusCode`"); 46 | Error::new(status, error) 47 | }) 48 | } 49 | 50 | /// Wrap the error value with an additional status code that is evaluated 51 | /// lazily only once an error does occur. 52 | /// 53 | /// # Panics 54 | /// 55 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 56 | /// 57 | /// [status]: crate::Status 58 | /// [statuscode]: crate::StatusCode 59 | fn with_status(self, f: F) -> Result 60 | where 61 | S: TryInto, 62 | S::Error: Debug, 63 | F: FnOnce() -> S, 64 | { 65 | self.map_err(|error| { 66 | let status = f() 67 | .try_into() 68 | .expect("Could not convert into a valid `StatusCode`"); 69 | Error::new(status, error) 70 | }) 71 | } 72 | } 73 | 74 | impl Status for Result { 75 | /// Wrap the error value with an additional status code. 76 | /// 77 | /// # Panics 78 | /// 79 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 80 | /// 81 | /// [status]: crate::Status 82 | /// [statuscode]: crate::StatusCode 83 | fn status(self, status: S) -> Result 84 | where 85 | S: TryInto, 86 | S::Error: Debug, 87 | { 88 | self.map_err(|mut error| { 89 | error.set_status(status); 90 | error 91 | }) 92 | } 93 | 94 | /// Wrap the error value with an additional status code that is evaluated 95 | /// lazily only once an error does occur. 96 | /// 97 | /// # Panics 98 | /// 99 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 100 | /// 101 | /// [status]: crate::Status 102 | /// [statuscode]: crate::StatusCode 103 | fn with_status(self, f: F) -> Result 104 | where 105 | S: TryInto, 106 | S::Error: Debug, 107 | F: FnOnce() -> S, 108 | { 109 | self.map_err(|mut error| { 110 | error.set_status(f()); 111 | error 112 | }) 113 | } 114 | } 115 | 116 | impl Status for Option { 117 | /// Wrap the error value with an additional status code. 118 | /// 119 | /// # Panics 120 | /// 121 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 122 | /// 123 | /// [status]: crate::Status 124 | /// [statuscode]: crate::StatusCode 125 | fn status(self, status: S) -> Result 126 | where 127 | S: TryInto, 128 | S::Error: Debug, 129 | { 130 | self.ok_or_else(|| { 131 | let status = status 132 | .try_into() 133 | .expect("Could not convert into a valid `StatusCode`"); 134 | Error::from_str(status, "NoneError") 135 | }) 136 | } 137 | 138 | /// Wrap the error value with an additional status code that is evaluated 139 | /// lazily only once an error does occur. 140 | /// 141 | /// # Panics 142 | /// 143 | /// Panics if [`Status`][status] is not a valid [`StatusCode`][statuscode]. 144 | /// 145 | /// [status]: crate::Status 146 | /// [statuscode]: crate::StatusCode 147 | fn with_status(self, f: F) -> Result 148 | where 149 | S: TryInto, 150 | S::Error: Debug, 151 | F: FnOnce() -> S, 152 | { 153 | self.ok_or_else(|| { 154 | let status = f() 155 | .try_into() 156 | .expect("Could not convert into a valid `StatusCode`"); 157 | Error::from_str(status, "NoneError") 158 | }) 159 | } 160 | } 161 | 162 | pub(crate) mod private { 163 | pub trait Sealed {} 164 | 165 | impl Sealed for Result {} 166 | impl Sealed for Option {} 167 | } 168 | 169 | #[cfg(test)] 170 | mod test { 171 | use super::Status; 172 | 173 | #[test] 174 | fn construct_shorthand_with_valid_status_code() { 175 | Some(()).status(200).unwrap(); 176 | } 177 | 178 | #[test] 179 | #[should_panic(expected = "Could not convert into a valid `StatusCode`")] 180 | fn construct_shorthand_with_invalid_status_code() { 181 | let res: Result<(), std::io::Error> = 182 | Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!")); 183 | res.status(600).unwrap(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/trace/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP timings and traces. 2 | //! 3 | //! This module implements parsers and serializers for timing-related headers. 4 | //! These headers enable tracing and timing requests, and help answer the 5 | //! question of: _"Where is my program spending its time?"_ 6 | //! 7 | //! # Specifications 8 | //! 9 | //! - [W3C Trace-Context header](https://w3c.github.io/trace-context/) 10 | //! - [W3C Server-Timing header](https://w3c.github.io/server-timing/#the-server-timing-header-field) 11 | 12 | pub mod server_timing; 13 | mod trace_context; 14 | 15 | #[doc(inline)] 16 | pub use server_timing::{Metric, ServerTiming}; 17 | pub use trace_context::TraceContext; 18 | -------------------------------------------------------------------------------- /src/trace/server_timing/metric.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::headers::HeaderValue; 4 | 5 | /// An individual entry into `ServerTiming`. 6 | // 7 | // # Implementation notes 8 | // 9 | // Four different cases are valid: 10 | // 11 | // 1. metric name only cache 12 | // 2. metric + value cache;dur=2.4 13 | // 3. metric + desc cache;desc="Cache Read" 14 | // 4. metric + value + desc cache;desc="Cache Read";dur=23.2 15 | // 16 | // Multiple different entries per line are supported; separated with a `,`. 17 | #[derive(Debug, Clone, Eq, PartialEq)] 18 | pub struct Metric { 19 | pub(crate) name: String, 20 | pub(crate) dur: Option, 21 | pub(crate) desc: Option, 22 | } 23 | 24 | impl Metric { 25 | /// Create a new instance of `Metric`. 26 | /// 27 | /// # Errors 28 | /// 29 | /// An error will be returned if the string values are invalid ASCII. 30 | pub fn new(name: String, dur: Option, desc: Option) -> crate::Result { 31 | crate::ensure!(name.is_ascii(), "Name should be valid ASCII"); 32 | if let Some(desc) = desc.as_ref() { 33 | crate::ensure!(desc.is_ascii(), "Description should be valid ASCII"); 34 | }; 35 | 36 | Ok(Self { name, dur, desc }) 37 | } 38 | 39 | /// The timing name. 40 | pub fn name(&self) -> &String { 41 | &self.name 42 | } 43 | 44 | /// The timing duration. 45 | pub fn duration(&self) -> Option { 46 | self.dur 47 | } 48 | 49 | /// The timing description. 50 | pub fn description(&self) -> Option<&str> { 51 | self.desc.as_deref() 52 | } 53 | } 54 | 55 | impl From for HeaderValue { 56 | fn from(entry: Metric) -> HeaderValue { 57 | let mut string = entry.name; 58 | 59 | // Format a `Duration` into the format that the spec expects. 60 | let f = |d: Duration| d.as_secs_f64() * 1000.0; 61 | 62 | match (entry.dur, entry.desc) { 63 | (Some(dur), Some(desc)) => { 64 | string.push_str(&format!("; dur={}; desc=\"{}\"", f(dur), desc)) 65 | } 66 | (Some(dur), None) => string.push_str(&format!("; dur={}", f(dur))), 67 | (None, Some(desc)) => string.push_str(&format!("; desc=\"{}\"", desc)), 68 | (None, None) => {} 69 | }; 70 | 71 | // SAFETY: we validate that the values are valid ASCII on creation. 72 | unsafe { HeaderValue::from_bytes_unchecked(string.into_bytes()) } 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod test { 78 | use super::*; 79 | use crate::headers::HeaderValue; 80 | use std::time::Duration; 81 | 82 | #[test] 83 | #[allow(clippy::redundant_clone)] 84 | fn encode() -> crate::Result<()> { 85 | let name = String::from("Server"); 86 | let dur = Duration::from_secs(1); 87 | let desc = String::from("A server timing"); 88 | 89 | let val: HeaderValue = Metric::new(name.clone(), None, None)?.into(); 90 | assert_eq!(val, "Server"); 91 | 92 | let val: HeaderValue = Metric::new(name.clone(), Some(dur), None)?.into(); 93 | assert_eq!(val, "Server; dur=1000"); 94 | 95 | let val: HeaderValue = Metric::new(name.clone(), None, Some(desc.clone()))?.into(); 96 | assert_eq!(val, r#"Server; desc="A server timing""#); 97 | 98 | let val: HeaderValue = Metric::new(name.clone(), Some(dur), Some(desc.clone()))?.into(); 99 | assert_eq!(val, r#"Server; dur=1000; desc="A server timing""#); 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/transfer/encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::HeaderValue; 2 | use std::fmt::{self, Display}; 3 | 4 | /// Available compression algorithms. 5 | /// 6 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#Directives) 7 | #[non_exhaustive] 8 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 9 | pub enum Encoding { 10 | /// Send a series of chunks. 11 | Chunked, 12 | /// The Gzip encoding. 13 | Gzip, 14 | /// The Deflate encoding. 15 | Deflate, 16 | /// The Brotli encoding. 17 | Brotli, 18 | /// The Zstd encoding. 19 | Zstd, 20 | /// No encoding. 21 | Identity, 22 | } 23 | 24 | impl Encoding { 25 | /// Parses a given string into its corresponding encoding. 26 | pub(crate) fn from_str(s: &str) -> Option { 27 | let s = s.trim(); 28 | 29 | // We're dealing with an empty string. 30 | if s.is_empty() { 31 | return None; 32 | } 33 | 34 | match s { 35 | "chunked" => Some(Encoding::Chunked), 36 | "gzip" => Some(Encoding::Gzip), 37 | "deflate" => Some(Encoding::Deflate), 38 | "br" => Some(Encoding::Brotli), 39 | "zstd" => Some(Encoding::Zstd), 40 | "identity" => Some(Encoding::Identity), 41 | _ => None, 42 | } 43 | } 44 | } 45 | 46 | impl Display for Encoding { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self { 49 | Encoding::Gzip => write!(f, "gzip"), 50 | Encoding::Deflate => write!(f, "deflate"), 51 | Encoding::Brotli => write!(f, "br"), 52 | Encoding::Zstd => write!(f, "zstd"), 53 | Encoding::Identity => write!(f, "identity"), 54 | Encoding::Chunked => write!(f, "chunked"), 55 | } 56 | } 57 | } 58 | 59 | impl From for HeaderValue { 60 | fn from(directive: Encoding) -> Self { 61 | let s = directive.to_string(); 62 | unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/transfer/encoding_proposal.rs: -------------------------------------------------------------------------------- 1 | use crate::ensure; 2 | use crate::headers::HeaderValue; 3 | use crate::transfer::Encoding; 4 | use crate::utils::parse_weight; 5 | 6 | use std::cmp::{Ordering, PartialEq}; 7 | use std::ops::{Deref, DerefMut}; 8 | 9 | /// A proposed `Encoding` in `AcceptEncoding`. 10 | /// 11 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE#Directives) 12 | #[derive(Debug, Clone, Copy, PartialEq)] 13 | pub struct EncodingProposal { 14 | /// The proposed encoding. 15 | pub(crate) encoding: Encoding, 16 | 17 | /// The weight of the proposal. 18 | /// 19 | /// This is a number between 0.0 and 1.0, and is max 3 decimal points. 20 | weight: Option, 21 | } 22 | 23 | impl EncodingProposal { 24 | /// Create a new instance of `EncodingProposal`. 25 | pub fn new(encoding: impl Into, weight: Option) -> crate::Result { 26 | if let Some(weight) = weight { 27 | ensure!( 28 | weight.is_sign_positive() && weight <= 1.0, 29 | "EncodingProposal should have a weight between 0.0 and 1.0" 30 | ) 31 | } 32 | 33 | Ok(Self { 34 | encoding: encoding.into(), 35 | weight, 36 | }) 37 | } 38 | 39 | /// Get the proposed encoding. 40 | pub fn encoding(&self) -> &Encoding { 41 | &self.encoding 42 | } 43 | 44 | /// Get the weight of the proposal. 45 | pub fn weight(&self) -> Option { 46 | self.weight 47 | } 48 | 49 | pub(crate) fn from_str(s: &str) -> crate::Result> { 50 | let mut parts = s.split(';'); 51 | let encoding = match Encoding::from_str(parts.next().unwrap()) { 52 | Some(encoding) => encoding, 53 | None => return Ok(None), 54 | }; 55 | let weight = parts.next().map(parse_weight).transpose()?; 56 | 57 | Ok(Some(Self::new(encoding, weight)?)) 58 | } 59 | } 60 | 61 | impl From for EncodingProposal { 62 | fn from(encoding: Encoding) -> Self { 63 | Self { 64 | encoding, 65 | weight: None, 66 | } 67 | } 68 | } 69 | 70 | impl PartialEq for EncodingProposal { 71 | fn eq(&self, other: &Encoding) -> bool { 72 | self.encoding == *other 73 | } 74 | } 75 | 76 | impl PartialEq for &EncodingProposal { 77 | fn eq(&self, other: &Encoding) -> bool { 78 | self.encoding == *other 79 | } 80 | } 81 | 82 | impl Deref for EncodingProposal { 83 | type Target = Encoding; 84 | fn deref(&self) -> &Self::Target { 85 | &self.encoding 86 | } 87 | } 88 | 89 | impl DerefMut for EncodingProposal { 90 | fn deref_mut(&mut self) -> &mut Self::Target { 91 | &mut self.encoding 92 | } 93 | } 94 | 95 | // NOTE: Firefox populates Accept-Encoding as `gzip, deflate, br`. This means 96 | // when parsing encodings we should choose the last value in the list under 97 | // equal weights. This impl doesn't know which value was passed later, so that 98 | // behavior needs to be handled separately. 99 | // 100 | // NOTE: This comparison does not include a notion of `*` (any value is valid). 101 | // that needs to be handled separately. 102 | impl PartialOrd for EncodingProposal { 103 | fn partial_cmp(&self, other: &Self) -> Option { 104 | match (self.weight, other.weight) { 105 | (Some(left), Some(right)) => left.partial_cmp(&right), 106 | (Some(_), None) => Some(Ordering::Greater), 107 | (None, Some(_)) => Some(Ordering::Less), 108 | (None, None) => None, 109 | } 110 | } 111 | } 112 | 113 | impl From for HeaderValue { 114 | fn from(entry: EncodingProposal) -> HeaderValue { 115 | let s = match entry.weight { 116 | Some(weight) => format!("{};q={:.3}", entry.encoding, weight), 117 | None => entry.encoding.to_string(), 118 | }; 119 | unsafe { HeaderValue::from_bytes_unchecked(s.into_bytes()) } 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod test { 125 | use super::*; 126 | 127 | #[test] 128 | fn smoke() { 129 | let _ = EncodingProposal::new(Encoding::Gzip, Some(0.0)).unwrap(); 130 | let _ = EncodingProposal::new(Encoding::Gzip, Some(0.5)).unwrap(); 131 | let _ = EncodingProposal::new(Encoding::Gzip, Some(1.0)).unwrap(); 132 | } 133 | 134 | #[test] 135 | fn error_code_500() { 136 | let err = EncodingProposal::new(Encoding::Gzip, Some(1.1)).unwrap_err(); 137 | assert_eq!(err.status(), 500); 138 | 139 | let err = EncodingProposal::new(Encoding::Gzip, Some(-0.1)).unwrap_err(); 140 | assert_eq!(err.status(), 500); 141 | 142 | let err = EncodingProposal::new(Encoding::Gzip, Some(-0.0)).unwrap_err(); 143 | assert_eq!(err.status(), 500); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/transfer/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP transfer headers. 2 | //! 3 | //! [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Transfer_coding) 4 | 5 | mod encoding; 6 | mod encoding_proposal; 7 | mod te; 8 | mod transfer_encoding; 9 | 10 | pub use encoding::Encoding; 11 | pub use encoding_proposal::EncodingProposal; 12 | pub use te::TE; 13 | pub use transfer_encoding::TransferEncoding; 14 | -------------------------------------------------------------------------------- /src/transfer/transfer_encoding.rs: -------------------------------------------------------------------------------- 1 | use crate::headers::{Header, HeaderName, HeaderValue, Headers, TRANSFER_ENCODING}; 2 | use crate::transfer::{Encoding, EncodingProposal}; 3 | 4 | use std::fmt::{self, Debug}; 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | /// The form of encoding used to safely transfer the payload body to the user. 8 | /// 9 | /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding) 10 | /// 11 | /// # Specifications 12 | /// 13 | /// - [RFC 7230, section 3.3.1: Transfer-Encoding](https://tools.ietf.org/html/rfc7230#section-3.3.1) 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// # fn main() -> http_types::Result<()> { 19 | /// # 20 | /// use http_types::Response; 21 | /// use http_types::transfer::{TransferEncoding, Encoding}; 22 | /// let mut encoding = TransferEncoding::new(Encoding::Chunked); 23 | /// 24 | /// let mut res = Response::new(200); 25 | /// res.insert_header(&encoding, &encoding); 26 | /// 27 | /// let encoding = TransferEncoding::from_headers(res)?.unwrap(); 28 | /// assert_eq!(encoding, &Encoding::Chunked); 29 | /// # 30 | /// # Ok(()) } 31 | /// ``` 32 | pub struct TransferEncoding { 33 | inner: Encoding, 34 | } 35 | 36 | impl TransferEncoding { 37 | /// Create a new instance of `CacheControl`. 38 | pub fn new(encoding: Encoding) -> Self { 39 | Self { inner: encoding } 40 | } 41 | 42 | /// Create a new instance from headers. 43 | pub fn from_headers(headers: impl AsRef) -> crate::Result> { 44 | let headers = match headers.as_ref().get(TRANSFER_ENCODING) { 45 | Some(headers) => headers, 46 | None => return Ok(None), 47 | }; 48 | 49 | let mut inner = None; 50 | 51 | for value in headers { 52 | if let Some(entry) = Encoding::from_str(value.as_str()) { 53 | inner = Some(entry); 54 | } 55 | } 56 | 57 | let inner = inner.expect("Headers instance with no entries found"); 58 | Ok(Some(Self { inner })) 59 | } 60 | 61 | /// Access the encoding kind. 62 | pub fn encoding(&self) -> Encoding { 63 | self.inner 64 | } 65 | } 66 | 67 | impl Header for TransferEncoding { 68 | fn header_name(&self) -> HeaderName { 69 | TRANSFER_ENCODING 70 | } 71 | fn header_value(&self) -> HeaderValue { 72 | self.inner.into() 73 | } 74 | } 75 | 76 | impl Deref for TransferEncoding { 77 | type Target = Encoding; 78 | fn deref(&self) -> &Self::Target { 79 | &self.inner 80 | } 81 | } 82 | 83 | impl DerefMut for TransferEncoding { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.inner 86 | } 87 | } 88 | 89 | impl PartialEq for TransferEncoding { 90 | fn eq(&self, other: &Encoding) -> bool { 91 | &self.inner == other 92 | } 93 | } 94 | 95 | impl PartialEq<&Encoding> for TransferEncoding { 96 | fn eq(&self, other: &&Encoding) -> bool { 97 | &&self.inner == other 98 | } 99 | } 100 | 101 | impl From for TransferEncoding { 102 | fn from(encoding: Encoding) -> Self { 103 | Self { inner: encoding } 104 | } 105 | } 106 | 107 | impl From<&Encoding> for TransferEncoding { 108 | fn from(encoding: &Encoding) -> Self { 109 | Self { inner: *encoding } 110 | } 111 | } 112 | 113 | impl From for TransferEncoding { 114 | fn from(encoding: EncodingProposal) -> Self { 115 | Self { 116 | inner: encoding.encoding, 117 | } 118 | } 119 | } 120 | 121 | impl From<&EncodingProposal> for TransferEncoding { 122 | fn from(encoding: &EncodingProposal) -> Self { 123 | Self { 124 | inner: encoding.encoding, 125 | } 126 | } 127 | } 128 | 129 | impl Debug for TransferEncoding { 130 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 131 | self.inner.fmt(f) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/upgrade/connection.rs: -------------------------------------------------------------------------------- 1 | use futures_lite::{io, prelude::*}; 2 | 3 | use std::fmt::{self, Debug}; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | /// An upgraded HTTP connection. 8 | pub struct Connection { 9 | inner: Box, 10 | } 11 | 12 | impl Debug for Connection { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | let inner = "Box"; 15 | f.debug_struct("Connection").field("inner", &inner).finish() 16 | } 17 | } 18 | 19 | impl Connection { 20 | /// Create a new instance of `Connection`. 21 | pub fn new(t: T) -> Self 22 | where 23 | T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static, 24 | { 25 | Self { inner: Box::new(t) } 26 | } 27 | } 28 | 29 | /// Trait to signal the requirements for an underlying connection type. 30 | pub trait InnerConnection: AsyncRead + AsyncWrite + Send + Sync + Unpin {} 31 | impl InnerConnection for T {} 32 | 33 | impl AsyncRead for Connection { 34 | fn poll_read( 35 | mut self: Pin<&mut Self>, 36 | cx: &mut Context<'_>, 37 | buf: &mut [u8], 38 | ) -> Poll> { 39 | Pin::new(&mut self.inner).poll_read(cx, buf) 40 | } 41 | } 42 | 43 | impl AsyncWrite for Connection { 44 | fn poll_write( 45 | mut self: Pin<&mut Self>, 46 | cx: &mut Context<'_>, 47 | buf: &[u8], 48 | ) -> Poll> { 49 | Pin::new(&mut self.inner).poll_write(cx, buf) 50 | } 51 | 52 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 53 | Pin::new(&mut self.inner).poll_flush(cx) 54 | } 55 | 56 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 57 | Pin::new(&mut self.inner).poll_close(cx) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/upgrade/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP protocol upgrades. 2 | //! 3 | //! In HTTP it's not uncommon to convert from one protocol to another. For 4 | //! example `HTTP/1.1` can upgrade a connection to websockets using the 5 | //! [upgrade header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism), 6 | //! while `HTTP/2` uses [a custom 7 | //! handshake](https://tools.ietf.org/html/rfc8441#section-5.1). Regardless of 8 | //! the HTTP version, changing protocols always involves some handshake, 9 | //! after which it is turned into a stream of bytes. This module provides 10 | //! primitives for upgrading from HTTP request-response pairs to alternate 11 | //! protocols. 12 | 13 | mod connection; 14 | mod receiver; 15 | mod sender; 16 | 17 | pub use connection::Connection; 18 | pub use receiver::Receiver; 19 | pub use sender::Sender; 20 | -------------------------------------------------------------------------------- /src/upgrade/receiver.rs: -------------------------------------------------------------------------------- 1 | use futures_lite::Stream; 2 | 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use crate::upgrade::Connection; 8 | 9 | /// The receiving half of a channel to send an upgraded connection. 10 | #[must_use = "Futures do nothing unless polled or .awaited"] 11 | #[derive(Debug)] 12 | pub struct Receiver { 13 | receiver: async_channel::Receiver, 14 | } 15 | 16 | impl Receiver { 17 | /// Create a new instance of `Receiver`. 18 | #[allow(unused)] 19 | pub(crate) fn new(receiver: async_channel::Receiver) -> Self { 20 | Self { receiver } 21 | } 22 | } 23 | 24 | impl Future for Receiver { 25 | type Output = Option; 26 | 27 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 28 | Pin::new(&mut self.receiver).poll_next(cx) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/upgrade/sender.rs: -------------------------------------------------------------------------------- 1 | use crate::upgrade::Connection; 2 | 3 | /// The sending half of a channel to send an upgraded connection. 4 | /// 5 | /// Unlike `async_channel::Sender` the `send` method on this type can only be 6 | /// called once, and cannot be cloned. That's because only a single instance of 7 | /// `Connection` should be created. 8 | #[derive(Debug)] 9 | pub struct Sender { 10 | sender: async_channel::Sender, 11 | } 12 | 13 | impl Sender { 14 | /// Create a new instance of `Sender`. 15 | #[doc(hidden)] 16 | pub fn new(sender: async_channel::Sender) -> Self { 17 | Self { sender } 18 | } 19 | 20 | /// Send a `Connection`. 21 | /// 22 | /// The channel will be consumed after having sent the connection. 23 | pub async fn send(self, conn: Connection) { 24 | let _ = self.sender.send(conn).await; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod date; 2 | 3 | pub(crate) use date::fmt_http_date; 4 | pub(crate) use date::parse_http_date; 5 | pub(crate) use date::HttpDate; 6 | 7 | use crate::{Error, Status, StatusCode}; 8 | 9 | use std::cmp::Ordering; 10 | use std::str::FromStr; 11 | 12 | /// Parse a weight of the form `q=0.123`. 13 | pub(crate) fn parse_weight(s: &str) -> crate::Result { 14 | let mut parts = s.split('='); 15 | if !matches!(parts.next(), Some("q")) { 16 | let mut err = Error::new_adhoc("invalid weight"); 17 | err.set_status(StatusCode::BadRequest); 18 | return Err(err); 19 | } 20 | match parts.next() { 21 | Some(s) => { 22 | let weight = f32::from_str(s).status(400)?; 23 | Ok(weight) 24 | } 25 | None => { 26 | let mut err = Error::new_adhoc("invalid weight"); 27 | err.set_status(StatusCode::BadRequest); 28 | Err(err) 29 | } 30 | } 31 | } 32 | 33 | /// Order proposals by weight. Try ordering by q value first. If equal or undefined, 34 | /// order by index, favoring the latest provided value. 35 | pub(crate) fn sort_by_weight(props: &mut Vec) { 36 | let mut arr: Vec<(usize, T)> = props.iter().cloned().enumerate().collect(); 37 | arr.sort_unstable_by(|a, b| match b.1.partial_cmp(&a.1) { 38 | None | Some(Ordering::Equal) => b.0.cmp(&a.0), 39 | Some(ord) => ord, 40 | }); 41 | *props = arr.into_iter().map(|(_, t)| t).collect::>(); 42 | } 43 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | /// The version of the HTTP protocol in use. 2 | #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] 3 | #[non_exhaustive] 4 | pub enum Version { 5 | /// HTTP/0.9 6 | Http0_9, 7 | 8 | /// HTTP/1.0 9 | Http1_0, 10 | 11 | /// HTTP/1.1 12 | Http1_1, 13 | 14 | /// HTTP/2.0 15 | Http2_0, 16 | 17 | /// HTTP/3.0 18 | Http3_0, 19 | } 20 | 21 | #[cfg(feature = "serde")] 22 | mod serde { 23 | use super::Version; 24 | use serde_crate::{ 25 | de::{Error, Unexpected, Visitor}, 26 | Deserialize, Deserializer, Serialize, Serializer, 27 | }; 28 | 29 | impl Serialize for Version { 30 | fn serialize(&self, serializer: S) -> Result 31 | where 32 | S: Serializer, 33 | { 34 | serializer.serialize_str(self.as_ref()) 35 | } 36 | } 37 | 38 | struct VersionVisitor; 39 | 40 | impl<'de> Visitor<'de> for VersionVisitor { 41 | type Value = Version; 42 | 43 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 44 | write!(formatter, "a HTTP version as &str") 45 | } 46 | 47 | fn visit_str(self, v: &str) -> Result 48 | where 49 | E: Error, 50 | { 51 | match v { 52 | "HTTP/0.9" => Ok(Version::Http0_9), 53 | "HTTP/1.0" => Ok(Version::Http1_0), 54 | "HTTP/1.1" => Ok(Version::Http1_1), 55 | "HTTP/2" => Ok(Version::Http2_0), 56 | "HTTP/3" => Ok(Version::Http3_0), 57 | _ => Err(Error::invalid_value(Unexpected::Str(v), &self)), 58 | } 59 | } 60 | 61 | fn visit_string(self, v: String) -> Result 62 | where 63 | E: Error, 64 | { 65 | self.visit_str(&v) 66 | } 67 | } 68 | 69 | impl<'de> Deserialize<'de> for Version { 70 | fn deserialize(deserializer: D) -> Result 71 | where 72 | D: Deserializer<'de>, 73 | { 74 | deserializer.deserialize_str(VersionVisitor) 75 | } 76 | } 77 | } 78 | 79 | impl AsRef for Version { 80 | fn as_ref(&self) -> &'static str { 81 | match self { 82 | Version::Http0_9 => "HTTP/0.9", 83 | Version::Http1_0 => "HTTP/1.0", 84 | Version::Http1_1 => "HTTP/1.1", 85 | Version::Http2_0 => "HTTP/2", 86 | Version::Http3_0 => "HTTP/3", 87 | } 88 | } 89 | } 90 | 91 | impl std::fmt::Display for Version { 92 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 93 | f.write_str(self.as_ref()) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod test { 99 | use super::*; 100 | 101 | #[test] 102 | fn as_ref() { 103 | assert_eq!(Version::Http0_9.as_ref(), "HTTP/0.9"); 104 | assert_eq!(Version::Http1_0.as_ref(), "HTTP/1.0"); 105 | assert_eq!(Version::Http1_1.as_ref(), "HTTP/1.1"); 106 | assert_eq!(Version::Http2_0.as_ref(), "HTTP/2"); 107 | assert_eq!(Version::Http3_0.as_ref(), "HTTP/3"); 108 | } 109 | 110 | #[test] 111 | fn to_string() { 112 | let output = format!( 113 | "{} {} {} {} {}", 114 | Version::Http0_9, 115 | Version::Http1_0, 116 | Version::Http1_1, 117 | Version::Http2_0, 118 | Version::Http3_0 119 | ); 120 | assert_eq!("HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3", output); 121 | } 122 | 123 | #[test] 124 | fn ord() { 125 | use Version::*; 126 | assert!(Http3_0 > Http2_0); 127 | assert!(Http2_0 > Http1_1); 128 | assert!(Http1_1 > Http1_0); 129 | assert!(Http1_0 > Http0_9); 130 | } 131 | 132 | #[test] 133 | fn serde() -> Result<(), serde_json::Error> { 134 | assert_eq!("\"HTTP/3\"", serde_json::to_string(&Version::Http3_0)?); 135 | assert_eq!(Version::Http1_1, serde_json::from_str("\"HTTP/1.1\"")?); 136 | Ok(()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/error.rs: -------------------------------------------------------------------------------- 1 | use http_types::{bail, ensure, ensure_eq, Error, StatusCode}; 2 | use std::io; 3 | 4 | #[test] 5 | fn can_be_boxed() { 6 | fn can_be_boxed() -> Result<(), Box> { 7 | let err = io::Error::new(io::ErrorKind::Other, "Oh no"); 8 | Err(Error::new(StatusCode::NotFound, err).into()) 9 | } 10 | assert!(can_be_boxed().is_err()); 11 | } 12 | 13 | #[test] 14 | fn internal_server_error_by_default() { 15 | fn run() -> http_types::Result<()> { 16 | Err(io::Error::new(io::ErrorKind::Other, "Oh no").into()) 17 | } 18 | let err = run().unwrap_err(); 19 | assert_eq!(err.status(), 500); 20 | } 21 | 22 | #[test] 23 | fn ensure() { 24 | fn inner() -> http_types::Result<()> { 25 | ensure!(true, "Oh yes"); 26 | bail!("Oh no!"); 27 | } 28 | let res = inner(); 29 | assert!(res.is_err()); 30 | let err = res.unwrap_err(); 31 | assert_eq!(err.status(), StatusCode::InternalServerError); 32 | } 33 | 34 | #[test] 35 | fn ensure_eq() { 36 | fn inner() -> http_types::Result<()> { 37 | ensure_eq!(1, 1, "Oh yes"); 38 | bail!("Oh no!"); 39 | } 40 | let res = inner(); 41 | assert!(res.is_err()); 42 | let err = res.unwrap_err(); 43 | assert_eq!(err.status(), StatusCode::InternalServerError); 44 | } 45 | 46 | #[test] 47 | fn result_ext() { 48 | use http_types::Status; 49 | fn run() -> http_types::Result<()> { 50 | let err = io::Error::new(io::ErrorKind::Other, "Oh no"); 51 | Err(err).status(StatusCode::NotFound)?; 52 | Ok(()) 53 | } 54 | let res = run(); 55 | assert!(res.is_err()); 56 | 57 | let err = res.unwrap_err(); 58 | assert_eq!(err.status(), StatusCode::NotFound); 59 | } 60 | 61 | #[test] 62 | fn option_ext() { 63 | use http_types::Status; 64 | fn run() -> http_types::Result<()> { 65 | None.status(StatusCode::NotFound) 66 | } 67 | let res = run(); 68 | assert!(res.is_err()); 69 | 70 | let err = res.unwrap_err(); 71 | assert_eq!(err.status(), StatusCode::NotFound); 72 | } 73 | 74 | #[test] 75 | fn anyhow_error_into_http_types_error() { 76 | let anyhow_error = 77 | anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::Other, "irrelevant")); 78 | let http_types_error: Error = anyhow_error.into(); 79 | assert_eq!(http_types_error.status(), StatusCode::InternalServerError); 80 | 81 | let anyhow_error = 82 | anyhow::Error::new(std::io::Error::new(std::io::ErrorKind::Other, "irrelevant")); 83 | let http_types_error: Error = Error::new(StatusCode::ImATeapot, anyhow_error); 84 | assert_eq!(http_types_error.status(), StatusCode::ImATeapot); 85 | } 86 | 87 | #[test] 88 | fn normal_error_into_http_types_error() { 89 | let http_types_error: Error = 90 | std::io::Error::new(std::io::ErrorKind::Other, "irrelevant").into(); 91 | assert_eq!(http_types_error.status(), StatusCode::InternalServerError); 92 | 93 | let http_types_error = Error::new( 94 | StatusCode::ImATeapot, 95 | std::io::Error::new(std::io::ErrorKind::Other, "irrelevant"), 96 | ); 97 | assert_eq!(http_types_error.status(), StatusCode::ImATeapot); 98 | } 99 | 100 | #[test] 101 | fn u16_into_status_code_in_http_types_error() { 102 | let http_types_error = Error::new(404, io::Error::new(io::ErrorKind::Other, "Not Found")); 103 | let http_types_error2 = Error::new( 104 | StatusCode::NotFound, 105 | io::Error::new(io::ErrorKind::Other, "Not Found"), 106 | ); 107 | assert_eq!(http_types_error.status(), http_types_error2.status()); 108 | 109 | let http_types_error = Error::from_str(404, "Not Found"); 110 | assert_eq!(http_types_error.status(), StatusCode::NotFound); 111 | } 112 | 113 | #[test] 114 | #[should_panic] 115 | fn fail_test_u16_into_status_code_in_http_types_error_new() { 116 | let _http_types_error = Error::new( 117 | 1000, 118 | io::Error::new(io::ErrorKind::Other, "Incorrect status code"), 119 | ); 120 | } 121 | 122 | #[test] 123 | #[should_panic] 124 | fn fail_test_u16_into_status_code_in_http_types_error_from_str() { 125 | let _http_types_error = Error::from_str(1000, "Incorrect status code"); 126 | } 127 | -------------------------------------------------------------------------------- /tests/fixtures/empty.custom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-rs/http-types/ac5d645ce5294554b86ebd49233d3ec01665d1d7/tests/fixtures/empty.custom -------------------------------------------------------------------------------- /tests/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a fixture! 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/fixtures/nori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/http-rs/http-types/ac5d645ce5294554b86ebd49233d3ec01665d1d7/tests/fixtures/nori.png -------------------------------------------------------------------------------- /tests/fixtures/unknown.custom: -------------------------------------------------------------------------------- 1 | this is an unknown text format 2 | -------------------------------------------------------------------------------- /tests/headers.rs: -------------------------------------------------------------------------------- 1 | // use http_types::{Response, StatusCode}; 2 | 3 | // #[test] 4 | // fn headers_cmp() { 5 | // let mut res = Response::new(StatusCode::Ok); 6 | // res.insert_header("content-type", "application/json"); 7 | // assert_eq!(res.header("content-type").unwrap(), "application/json"); 8 | // } 9 | -------------------------------------------------------------------------------- /tests/mime.rs: -------------------------------------------------------------------------------- 1 | #[cfg(features = "fs")] 2 | mod tests { 3 | use async_std::fs; 4 | use async_std::io; 5 | use http_types::{mime, Body, Response}; 6 | 7 | #[async_std::test] 8 | async fn guess_plain_text_mime() -> io::Result<()> { 9 | let body = Body::from_path("tests/fixtures/index.html").await?; 10 | let mut res = Response::new(200); 11 | res.set_body(body); 12 | assert_eq!(res.content_type(), Some(mime::HTML)); 13 | Ok(()) 14 | } 15 | 16 | #[async_std::test] 17 | async fn guess_binary_mime() -> http_types::Result<()> { 18 | let body = Body::from_path("tests/fixtures/nori.png").await?; 19 | let mut res = Response::new(200); 20 | res.set_body(body); 21 | assert_eq!(res.content_type(), Some(mime::PNG)); 22 | 23 | // Assert the file is correctly reset after we've peeked the bytes 24 | let left = fs::read("tests/fixtures/nori.png").await?; 25 | let right = res.body_bytes().await?; 26 | assert_eq!(left, right); 27 | Ok(()) 28 | } 29 | 30 | #[async_std::test] 31 | async fn guess_mime_fallback() -> io::Result<()> { 32 | let body = Body::from_path("tests/fixtures/unknown.custom").await?; 33 | let mut res = Response::new(200); 34 | res.set_body(body); 35 | assert_eq!(res.content_type(), Some(mime::BYTE_STREAM)); 36 | Ok(()) 37 | } 38 | 39 | #[async_std::test] 40 | async fn parse_empty_files() -> http_types::Result<()> { 41 | let body = Body::from_path("tests/fixtures/empty.custom").await?; 42 | let mut res = Response::new(200); 43 | res.set_body(body); 44 | assert_eq!(res.content_type(), Some(mime::BYTE_STREAM)); 45 | Ok(()) 46 | } 47 | 48 | // #[test] 49 | // fn match_mime_types() { 50 | // let req = Request::get("https://example.com"); 51 | // match req.content_type() { 52 | // Some(mime::JSON) => {} 53 | // _ => {} 54 | // } 55 | // } 56 | } 57 | -------------------------------------------------------------------------------- /tests/querystring.rs: -------------------------------------------------------------------------------- 1 | use http_types::{url::Url, Method}; 2 | use serde_crate::Deserialize; 3 | 4 | #[derive(Deserialize)] 5 | #[serde(crate = "serde_crate")] 6 | struct Params { 7 | msg: String, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | #[serde(crate = "serde_crate")] 12 | struct OptionalParams { 13 | _msg: Option, 14 | _time: Option, 15 | } 16 | 17 | #[test] 18 | fn successfully_deserialize_query() { 19 | let req = http_types::Request::new( 20 | Method::Get, 21 | Url::parse("http://example.com/?msg=Hello").unwrap(), 22 | ); 23 | 24 | let params = req.query::(); 25 | assert!(params.is_ok()); 26 | assert_eq!(params.unwrap().msg, "Hello"); 27 | } 28 | 29 | #[test] 30 | fn unsuccessfully_deserialize_query() { 31 | let req = http_types::Request::new(Method::Get, Url::parse("http://example.com/").unwrap()); 32 | 33 | let params = req.query::(); 34 | assert!(params.is_err()); 35 | assert_eq!(params.err().unwrap().to_string(), "missing field `msg`"); 36 | } 37 | 38 | #[test] 39 | fn malformatted_query() { 40 | let req = http_types::Request::new( 41 | Method::Get, 42 | Url::parse("http://example.com/?error=should_fail").unwrap(), 43 | ); 44 | 45 | let params = req.query::(); 46 | assert!(params.is_err()); 47 | assert_eq!(params.err().unwrap().to_string(), "missing field `msg`"); 48 | } 49 | 50 | #[test] 51 | fn empty_query_string_for_struct_with_no_required_fields() { 52 | let req = http_types::Request::new(Method::Get, Url::parse("http://example.com").unwrap()); 53 | 54 | let params = req.query::(); 55 | assert!(params.is_ok()); 56 | } 57 | -------------------------------------------------------------------------------- /tests/req_res_body.rs: -------------------------------------------------------------------------------- 1 | use futures_lite::{future, AsyncReadExt}; 2 | use http_types::{Body, Method, Request, Response, StatusCode, Url}; 3 | 4 | #[test] 5 | fn test_req_res_set_body() { 6 | let mut req = Request::new(Method::Get, Url::parse("http://example.com/").unwrap()); 7 | req.set_body(Body::empty()); 8 | let mut res = Response::new(StatusCode::Ok); 9 | res.set_body(req); 10 | let body = future::block_on(async move { 11 | let mut body = Vec::new(); 12 | res.read_to_end(&mut body).await.unwrap(); 13 | body 14 | }); 15 | assert!(body.is_empty()); 16 | } 17 | 18 | #[test] 19 | fn test_req_res_take_replace_body() { 20 | let mut req = Request::new(Method::Get, Url::parse("http://example.com/").unwrap()); 21 | req.take_body(); 22 | let mut res = Response::new(StatusCode::Ok); 23 | res.replace_body(req); 24 | let body = future::block_on(async move { 25 | let mut body = Vec::new(); 26 | res.read_to_end(&mut body).await.unwrap(); 27 | body 28 | }); 29 | assert!(body.is_empty()); 30 | } 31 | -------------------------------------------------------------------------------- /tests/security.rs: -------------------------------------------------------------------------------- 1 | use http_types::{security, Response, StatusCode}; 2 | 3 | #[test] 4 | fn security_test() { 5 | let mut policy = security::ContentSecurityPolicy::new(); 6 | policy 7 | .default_src(security::Source::SameOrigin) 8 | .default_src("areweasyncyet.rs") 9 | .script_src(security::Source::SameOrigin) 10 | .script_src(security::Source::UnsafeInline) 11 | .object_src(security::Source::None) 12 | .base_uri(security::Source::None) 13 | .upgrade_insecure_requests(); 14 | 15 | let mut res = Response::new(StatusCode::Ok); 16 | res.set_body("Hello, Chashu!"); 17 | 18 | security::default(&mut res); 19 | policy.apply(&mut res); 20 | 21 | assert_eq!(res["content-security-policy"], "base-uri 'none'; default-src 'self' areweasyncyet.rs; object-src 'none'; script-src 'self' 'unsafe-inline'; upgrade-insecure-requests"); 22 | } 23 | --------------------------------------------------------------------------------