1 use std::convert::TryFrom;
2 
3 use bytes::Bytes;
4 
5 /// A reason phrase in an HTTP/1 response.
6 ///
7 /// # Clients
8 ///
9 /// For clients, a `ReasonPhrase` will be present in the extensions of the `http::Response` returned
10 /// for a request if the reason phrase is different from the canonical reason phrase for the
11 /// response's status code. For example, if a server returns `HTTP/1.1 200 Awesome`, the
12 /// `ReasonPhrase` will be present and contain `Awesome`, but if a server returns `HTTP/1.1 200 OK`,
13 /// the response will not contain a `ReasonPhrase`.
14 ///
15 /// ```no_run
16 /// # #[cfg(all(feature = "tcp", feature = "client", feature = "http1"))]
17 /// # async fn fake_fetch() -> hyper::Result<()> {
18 /// use hyper::{Client, Uri};
19 /// use hyper::ext::ReasonPhrase;
20 ///
21 /// let res = Client::new().get(Uri::from_static("http://example.com/non_canonical_reason")).await?;
22 ///
23 /// // Print out the non-canonical reason phrase, if it has one...
24 /// if let Some(reason) = res.extensions().get::<ReasonPhrase>() {
25 ///     println!("non-canonical reason: {}", std::str::from_utf8(reason.as_bytes()).unwrap());
26 /// }
27 /// # Ok(())
28 /// # }
29 /// ```
30 ///
31 /// # Servers
32 ///
33 /// When a `ReasonPhrase` is present in the extensions of the `http::Response` written by a server,
34 /// its contents will be written in place of the canonical reason phrase when responding via HTTP/1.
35 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
36 pub struct ReasonPhrase(Bytes);
37 
38 impl ReasonPhrase {
39     /// Gets the reason phrase as bytes.
as_bytes(&self) -> &[u8]40     pub fn as_bytes(&self) -> &[u8] {
41         &self.0
42     }
43 
44     /// Converts a static byte slice to a reason phrase.
from_static(reason: &'static [u8]) -> Self45     pub fn from_static(reason: &'static [u8]) -> Self {
46         // TODO: this can be made const once MSRV is >= 1.57.0
47         if find_invalid_byte(reason).is_some() {
48             panic!("invalid byte in static reason phrase");
49         }
50         Self(Bytes::from_static(reason))
51     }
52 
53     /// Converts a `Bytes` directly into a `ReasonPhrase` without validating.
54     ///
55     /// Use with care; invalid bytes in a reason phrase can cause serious security problems if
56     /// emitted in a response.
from_bytes_unchecked(reason: Bytes) -> Self57     pub unsafe fn from_bytes_unchecked(reason: Bytes) -> Self {
58         Self(reason)
59     }
60 }
61 
62 impl TryFrom<&[u8]> for ReasonPhrase {
63     type Error = InvalidReasonPhrase;
64 
try_from(reason: &[u8]) -> Result<Self, Self::Error>65     fn try_from(reason: &[u8]) -> Result<Self, Self::Error> {
66         if let Some(bad_byte) = find_invalid_byte(reason) {
67             Err(InvalidReasonPhrase { bad_byte })
68         } else {
69             Ok(Self(Bytes::copy_from_slice(reason)))
70         }
71     }
72 }
73 
74 impl TryFrom<Vec<u8>> for ReasonPhrase {
75     type Error = InvalidReasonPhrase;
76 
try_from(reason: Vec<u8>) -> Result<Self, Self::Error>77     fn try_from(reason: Vec<u8>) -> Result<Self, Self::Error> {
78         if let Some(bad_byte) = find_invalid_byte(&reason) {
79             Err(InvalidReasonPhrase { bad_byte })
80         } else {
81             Ok(Self(Bytes::from(reason)))
82         }
83     }
84 }
85 
86 impl TryFrom<String> for ReasonPhrase {
87     type Error = InvalidReasonPhrase;
88 
try_from(reason: String) -> Result<Self, Self::Error>89     fn try_from(reason: String) -> Result<Self, Self::Error> {
90         if let Some(bad_byte) = find_invalid_byte(reason.as_bytes()) {
91             Err(InvalidReasonPhrase { bad_byte })
92         } else {
93             Ok(Self(Bytes::from(reason)))
94         }
95     }
96 }
97 
98 impl TryFrom<Bytes> for ReasonPhrase {
99     type Error = InvalidReasonPhrase;
100 
try_from(reason: Bytes) -> Result<Self, Self::Error>101     fn try_from(reason: Bytes) -> Result<Self, Self::Error> {
102         if let Some(bad_byte) = find_invalid_byte(&reason) {
103             Err(InvalidReasonPhrase { bad_byte })
104         } else {
105             Ok(Self(reason))
106         }
107     }
108 }
109 
110 impl Into<Bytes> for ReasonPhrase {
into(self) -> Bytes111     fn into(self) -> Bytes {
112         self.0
113     }
114 }
115 
116 impl AsRef<[u8]> for ReasonPhrase {
as_ref(&self) -> &[u8]117     fn as_ref(&self) -> &[u8] {
118         &self.0
119     }
120 }
121 
122 /// Error indicating an invalid byte when constructing a `ReasonPhrase`.
123 ///
124 /// See [the spec][spec] for details on allowed bytes.
125 ///
126 /// [spec]: https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
127 #[derive(Debug)]
128 pub struct InvalidReasonPhrase {
129     bad_byte: u8,
130 }
131 
132 impl std::fmt::Display for InvalidReasonPhrase {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result133     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134         write!(f, "Invalid byte in reason phrase: {}", self.bad_byte)
135     }
136 }
137 
138 impl std::error::Error for InvalidReasonPhrase {}
139 
is_valid_byte(b: u8) -> bool140 const fn is_valid_byte(b: u8) -> bool {
141     // See https://www.rfc-editor.org/rfc/rfc5234.html#appendix-B.1
142     const fn is_vchar(b: u8) -> bool {
143         0x21 <= b && b <= 0x7E
144     }
145 
146     // See https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values
147     //
148     // The 0xFF comparison is technically redundant, but it matches the text of the spec more
149     // clearly and will be optimized away.
150     #[allow(unused_comparisons)]
151     const fn is_obs_text(b: u8) -> bool {
152         0x80 <= b && b <= 0xFF
153     }
154 
155     // See https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#rfc.section.4.p.7
156     b == b'\t' || b == b' ' || is_vchar(b) || is_obs_text(b)
157 }
158 
find_invalid_byte(bytes: &[u8]) -> Option<u8>159 const fn find_invalid_byte(bytes: &[u8]) -> Option<u8> {
160     let mut i = 0;
161     while i < bytes.len() {
162         let b = bytes[i];
163         if !is_valid_byte(b) {
164             return Some(b);
165         }
166         i += 1;
167     }
168     None
169 }
170 
171 #[cfg(test)]
172 mod tests {
173     use super::*;
174 
175     #[test]
basic_valid()176     fn basic_valid() {
177         const PHRASE: &'static [u8] = b"OK";
178         assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
179         assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
180     }
181 
182     #[test]
empty_valid()183     fn empty_valid() {
184         const PHRASE: &'static [u8] = b"";
185         assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
186         assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
187     }
188 
189     #[test]
obs_text_valid()190     fn obs_text_valid() {
191         const PHRASE: &'static [u8] = b"hyp\xe9r";
192         assert_eq!(ReasonPhrase::from_static(PHRASE).as_bytes(), PHRASE);
193         assert_eq!(ReasonPhrase::try_from(PHRASE).unwrap().as_bytes(), PHRASE);
194     }
195 
196     const NEWLINE_PHRASE: &'static [u8] = b"hyp\ner";
197 
198     #[test]
199     #[should_panic]
newline_invalid_panic()200     fn newline_invalid_panic() {
201         ReasonPhrase::from_static(NEWLINE_PHRASE);
202     }
203 
204     #[test]
newline_invalid_err()205     fn newline_invalid_err() {
206         assert!(ReasonPhrase::try_from(NEWLINE_PHRASE).is_err());
207     }
208 
209     const CR_PHRASE: &'static [u8] = b"hyp\rer";
210 
211     #[test]
212     #[should_panic]
cr_invalid_panic()213     fn cr_invalid_panic() {
214         ReasonPhrase::from_static(CR_PHRASE);
215     }
216 
217     #[test]
cr_invalid_err()218     fn cr_invalid_err() {
219         assert!(ReasonPhrase::try_from(CR_PHRASE).is_err());
220     }
221 }
222