use std::convert::TryFrom; use bytes::Bytes; /// A reason phrase in an HTTP/1 response. /// /// # Clients /// /// For clients, a `ReasonPhrase` will be present in the extensions of the `http::Response` returned /// for a request if the reason phrase is different from the canonical reason phrase for the /// response's status code. For example, if a server returns `HTTP/1.1 200 Awesome`, the /// `ReasonPhrase` will be present and contain `Awesome`, but if a server returns `HTTP/1.1 200 OK`, /// the response will not contain a `ReasonPhrase`. /// /// ```no_run /// # #[cfg(all(feature = "tcp", feature = "client", feature = "http1"))] /// # async fn fake_fetch() -> hyper::Result<()> { /// use hyper::{Client, Uri}; /// use hyper::ext::ReasonPhrase; /// /// let res = Client::new().get(Uri::from_static("http://example.com/non_canonical_reason")).await?; /// /// // Print out the non-canonical reason phrase, if it has one... /// if let Some(reason) = res.extensions().get::() { /// println!("non-canonical reason: {}", std::str::from_utf8(reason.as_bytes()).unwrap()); /// } /// # Ok(()) /// # } /// ``` /// /// # Servers /// /// When a `ReasonPhrase` is present in the extensions of the `http::Response` written by a server, /// its contents will be written in place of the canonical reason phrase when responding via HTTP/1. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ReasonPhrase(Bytes); impl ReasonPhrase { /// Gets the reason phrase as bytes. pub fn as_bytes(&self) -> &[u8] { &self.0 } /// Converts a static byte slice to a reason phrase. pub fn from_static(reason: &'static [u8]) -> Self { // TODO: this can be made const once MSRV is >= 1.57.0 if find_invalid_byte(reason).is_some() { panic!("invalid byte in static reason phrase"); } Self(Bytes::from_static(reason)) } /// Converts a `Bytes` directly into a `ReasonPhrase` without validating. /// /// Use with care; invalid bytes in a reason phrase can cause serious security problems if /// emitted in a response. pub unsafe fn from_bytes_unchecked(reason: Bytes) -> Self { Self(reason) } } impl TryFrom<&[u8]> for ReasonPhrase { type Error = InvalidReasonPhrase; fn try_from(reason: &[u8]) -> Result { if let Some(bad_byte) = find_invalid_byte(reason) { Err(InvalidReasonPhrase { bad_byte }) } else { Ok(Self(Bytes::copy_from_slice(reason))) } } } impl TryFrom> for ReasonPhrase { type Error = InvalidReasonPhrase; fn try_from(reason: Vec) -> Result { if let Some(bad_byte) = find_invalid_byte(&reason) { Err(InvalidReasonPhrase { bad_byte }) } else { Ok(Self(Bytes::from(reason))) } } } impl TryFrom for ReasonPhrase { type Error = InvalidReasonPhrase; fn try_from(reason: String) -> Result { if let Some(bad_byte) = find_invalid_byte(reason.as_bytes()) { Err(InvalidReasonPhrase { bad_byte }) } else { Ok(Self(Bytes::from(reason))) } } } impl TryFrom for ReasonPhrase { type Error = InvalidReasonPhrase; fn try_from(reason: Bytes) -> Result { if let Some(bad_byte) = find_invalid_byte(&reason) { Err(InvalidReasonPhrase { bad_byte }) } else { Ok(Self(reason)) } } } impl Into for ReasonPhrase { fn into(self) -> Bytes { self.0 } } impl AsRef<[u8]> for ReasonPhrase { fn as_ref(&self) -> &[u8] { &self.0 } } /// Error indicating an invalid byte when constructing a `ReasonPhrase`. /// /// See [the spec][spec] for details on allowed bytes. /// /// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7 #[derive(Debug)] pub struct InvalidReasonPhrase { bad_byte: u8, } impl std::fmt::Display for InvalidReasonPhrase { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Invalid byte in reason phrase: {}", self.bad_byte) } } impl std::error::Error for InvalidReasonPhrase {} const fn is_valid_byte(b: u8) -> bool { // See https://www.rfc-editor.org/rfc/rfc5234.html#appendix-B.1 const fn is_vchar(b: u8) -> bool { 0x21 <= b && b <= 0x7E } // See https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values // // The 0xFF comparison is technically redundant, but it matches the text of the spec more // clearly and will be optimized away. #[allow(unused_comparisons)] const fn is_obs_text(b: u8) -> bool { 0x80 <= b && b <= 0xFF } // See https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7 b == b'\t' || b == b' ' || is_vchar(b) || is_obs_text(b) } const fn find_invalid_byte(bytes: &[u8]) -> Option { let mut i = 0; while i < bytes.len() { let b = bytes[i]; if !is_valid_byte(b) { return Some(b); } i += 1; } None } #[cfg(test)] mod tests { use super::*; #[test] fn basic_valid() { const PHRASE: &'static [u8] = b"OK"; assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE); assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE); } #[test] fn empty_valid() { const PHRASE: &'static [u8] = b""; assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE); assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE); } #[test] fn obs_text_valid() { const PHRASE: &'static [u8] = b"hyp\xe9r"; assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE); assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE); } const NEWLINE_PHRASE: &'static [u8] = b"hyp\ner"; #[test] #[should_panic] fn newline_invalid_panic() { ReasonPhrase::from_static(NEWLINE_PHRASE); } #[test] fn newline_invalid_err() { assert!(ReasonPhrase::try_from(NEWLINE_PHRASE).is_err()); } const CR_PHRASE: &'static [u8] = b"hyp\rer"; #[test] #[should_panic] fn cr_invalid_panic() { ReasonPhrase::from_static(CR_PHRASE); } #[test] fn cr_invalid_err() { assert!(ReasonPhrase::try_from(CR_PHRASE).is_err()); } }