Skip to main content

ntex/http/
httpmessage.rs

1use std::cell::{Ref, RefMut};
2use std::str;
3
4use encoding_rs::{Encoding, UTF_8};
5use mime::Mime;
6use ntex_http::header;
7
8#[cfg(feature = "cookie")]
9use coo_kie::Cookie;
10
11use super::error::{ContentTypeError, DecodeError};
12use super::header::HeaderMap;
13use crate::util::Extensions;
14
15#[cfg(feature = "cookie")]
16struct Cookies(Vec<Cookie<'static>>);
17
18/// Common operations for HTTP requests and responses.
19pub trait HttpMessage: Sized {
20    /// Returns the message headers.
21    fn message_headers(&self) -> &HeaderMap;
22
23    /// Returns the message extensions.
24    fn message_extensions(&self) -> Ref<'_, Extensions>;
25
26    /// Returns mutable access to the message extensions.
27    fn message_extensions_mut(&self) -> RefMut<'_, Extensions>;
28
29    /// Returns the media type from the `Content-Type` header.
30    ///
31    /// Parameters such as `charset` are omitted. Returns an empty string when
32    /// the header is missing or is not valid UTF-8.
33    fn content_type(&self) -> &str {
34        if let Some(content_type) = self.message_headers().get(header::CONTENT_TYPE)
35            && let Ok(content_type) = content_type.to_str()
36        {
37            return content_type.split(';').next().unwrap().trim();
38        }
39        ""
40    }
41
42    /// Returns the character encoding declared by `Content-Type`.
43    ///
44    /// UTF-8 is returned when the header or its `charset` parameter is absent.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`ContentTypeError::ParseError`] for an invalid media type or
49    /// [`ContentTypeError::UnknownEncoding`] for an unknown charset.
50    fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
51        if let Some(mime_type) = self.mime_type()? {
52            if let Some(charset) = mime_type.get_param("charset") {
53                if let Some(enc) = Encoding::for_label_no_replacement(charset.as_str().as_bytes()) {
54                    Ok(enc)
55                } else {
56                    Err(ContentTypeError::UnknownEncoding)
57                }
58            } else {
59                Ok(UTF_8)
60            }
61        } else {
62            Ok(UTF_8)
63        }
64    }
65
66    /// Parses the `Content-Type` header as a MIME type.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`ContentTypeError::ParseError`] when the header is not valid
71    /// UTF-8 or does not contain a valid MIME value.
72    fn mime_type(&self) -> Result<Option<Mime>, ContentTypeError> {
73        if let Some(content_type) = self.message_headers().get(header::CONTENT_TYPE) {
74            if let Ok(content_type) = content_type.to_str() {
75                return match content_type.parse() {
76                    Ok(mt) => Ok(Some(mt)),
77                    Err(_) => Err(ContentTypeError::ParseError),
78                };
79            }
80            Err(ContentTypeError::ParseError)
81        } else {
82            Ok(None)
83        }
84    }
85
86    /// Returns whether `Transfer-Encoding` contains `chunked`.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`DecodeError::Header`] when the header value is not valid
91    /// UTF-8.
92    fn chunked(&self) -> Result<bool, DecodeError> {
93        if let Some(encodings) = self.message_headers().get(header::TRANSFER_ENCODING) {
94            if let Ok(s) = encodings.to_str() {
95                Ok(s.to_lowercase().contains("chunked"))
96            } else {
97                Err(DecodeError::Header)
98            }
99        } else {
100            Ok(false)
101        }
102    }
103
104    #[cfg(feature = "cookie")]
105    /// Parses and caches cookies from the `Cookie` headers.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if a header is not valid UTF-8 or a cookie cannot be
110    /// parsed.
111    fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, coo_kie::ParseError> {
112        if self.message_extensions().get::<Cookies>().is_none() {
113            let mut cookies = Vec::new();
114            for hdr in self.message_headers().get_all(header::COOKIE) {
115                let s = str::from_utf8(hdr.as_bytes()).map_err(coo_kie::ParseError::from)?;
116                for cookie_str in s.split(';').map(str::trim) {
117                    if !cookie_str.is_empty() {
118                        cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned());
119                    }
120                }
121            }
122            self.message_extensions_mut().insert(Cookies(cookies));
123        }
124        Ok(Ref::map(self.message_extensions(), |ext| {
125            &ext.get::<Cookies>().unwrap().0
126        }))
127    }
128
129    #[cfg(feature = "cookie")]
130    /// Returns the first request cookie with the given name.
131    fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
132        if let Ok(cookies) = self.cookies() {
133            for cookie in cookies.iter() {
134                if cookie.name() == name {
135                    return Some(cookie.to_owned());
136                }
137            }
138        }
139        None
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use encoding_rs::ISO_8859_2;
146
147    use super::*;
148    use crate::http::test::TestRequest;
149
150    #[test]
151    fn test_content_type() {
152        let req = TestRequest::with_header("content-type", "text/plain").build();
153        assert_eq!(req.content_type(), "text/plain");
154        let req =
155            TestRequest::with_header("content-type", "application/json; charset=utf=8").build();
156        assert_eq!(req.content_type(), "application/json");
157        let req = TestRequest::builder().build();
158        assert_eq!(req.content_type(), "");
159    }
160
161    #[test]
162    fn test_mime_type() {
163        let req = TestRequest::with_header("content-type", "application/json").build();
164        assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON));
165        let req = TestRequest::builder().build();
166        assert_eq!(req.mime_type().unwrap(), None);
167        let req =
168            TestRequest::with_header("content-type", "application/json; charset=utf-8").build();
169        let mt = req.mime_type().unwrap().unwrap();
170        assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8));
171        assert_eq!(mt.type_(), mime::APPLICATION);
172        assert_eq!(mt.subtype(), mime::JSON);
173    }
174
175    #[test]
176    fn test_mime_type_error() {
177        let req =
178            TestRequest::with_header("content-type", "applicationadfadsfasdflknadsfklnadsfjson")
179                .build();
180        assert_eq!(Err(ContentTypeError::ParseError), req.mime_type());
181    }
182
183    #[test]
184    fn test_encoding() {
185        let req = TestRequest::builder().build();
186        assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
187
188        let req = TestRequest::with_header("content-type", "application/json").build();
189        assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
190
191        let req = TestRequest::with_header("content-type", "application/json; charset=ISO-8859-2")
192            .build();
193        assert_eq!(ISO_8859_2, req.encoding().unwrap());
194    }
195
196    #[test]
197    fn test_encoding_error() {
198        let req = TestRequest::with_header("content-type", "applicatjson").build();
199        assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err());
200
201        let req =
202            TestRequest::with_header("content-type", "application/json; charset=kkkttktk").build();
203        assert_eq!(
204            Some(ContentTypeError::UnknownEncoding),
205            req.encoding().err()
206        );
207    }
208
209    #[test]
210    fn test_chunked() {
211        let req = TestRequest::builder().build();
212        assert!(!req.chunked().unwrap());
213
214        let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").build();
215        assert!(req.chunked().unwrap());
216
217        let req = TestRequest::builder()
218            .header(
219                header::TRANSFER_ENCODING,
220                b"some va\xadscc\xacas0xsdasdlue".as_ref(),
221            )
222            .build();
223        assert!(req.chunked().is_err());
224    }
225
226    #[test]
227    fn test_mime_type_not_utf8() {
228        let req = TestRequest::default()
229            .header(
230                header::CONTENT_TYPE,
231                header::HeaderValue::from_bytes(b"text/\xff").unwrap(),
232            )
233            .build();
234        assert_eq!(req.mime_type(), Err(ContentTypeError::ParseError));
235    }
236
237    #[cfg(feature = "cookie")]
238    #[test]
239    fn test_cookie_not_found() {
240        let req = TestRequest::default()
241            .header(header::COOKIE, "c1=v1; ")
242            .build();
243        assert_eq!(req.cookies().unwrap().len(), 1);
244        assert!(req.cookie("c2").is_none());
245    }
246}