From e9fe3879df46fd066ca4bd3abd27919a0eadbfbf Mon Sep 17 00:00:00 2001 From: Phil Booth Date: Tue, 18 Dec 2018 17:53:03 +0000 Subject: [PATCH] Support custom content types in JsonConfig --- CHANGES.md | 4 +++ src/httpmessage.rs | 2 +- src/json.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 45faf35b5..232d85b8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [0.7.17] - 2018-xx-xx +### Added + +* Support for custom content types in `JsonConfig`. #637 + ### Fixed * HTTP1 decoder should perform case-insentive comparison for client requests (e.g. `Keep-Alive`). #631 diff --git a/src/httpmessage.rs b/src/httpmessage.rs index e96dce48c..ea5e4d862 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -200,7 +200,7 @@ pub trait HttpMessage: Sized { /// # fn main() {} /// ``` fn json(&self) -> JsonBody { - JsonBody::new(self) + JsonBody::new::<()>(self, None) } /// Return stream to http payload processes as multipart. diff --git a/src/json.rs b/src/json.rs index 178143f11..b04cad2fb 100644 --- a/src/json.rs +++ b/src/json.rs @@ -143,7 +143,7 @@ where let req2 = req.clone(); let err = Rc::clone(&cfg.ehandler); Box::new( - JsonBody::new(req) + JsonBody::new(req, Some(cfg)) .limit(cfg.limit) .map_err(move |e| (*err)(e, &req2)) .map(Json), @@ -155,6 +155,7 @@ where /// /// ```rust /// # extern crate actix_web; +/// extern crate mime; /// #[macro_use] extern crate serde_derive; /// use actix_web::{error, http, App, HttpResponse, Json, Result}; /// @@ -173,6 +174,9 @@ where /// r.method(http::Method::POST) /// .with_config(index, |cfg| { /// cfg.0.limit(4096) // <- change json extractor configuration +/// .content_type(|mime| { // <- accept text/plain content type +/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN +/// }) /// .error_handler(|err, req| { // <- create custom error response /// error::InternalError::from_response( /// err, HttpResponse::Conflict().finish()).into() @@ -184,6 +188,7 @@ where pub struct JsonConfig { limit: usize, ehandler: Rc) -> Error>, + content_type: Option bool>>, } impl JsonConfig { @@ -201,6 +206,15 @@ impl JsonConfig { self.ehandler = Rc::new(f); self } + + /// Set predicate for allowed content types + pub fn content_type(&mut self, predicate: F) -> &mut Self + where + F: Fn(mime::Mime) -> bool + 'static, + { + self.content_type = Some(Box::new(predicate)); + self + } } impl Default for JsonConfig { @@ -208,6 +222,7 @@ impl Default for JsonConfig { JsonConfig { limit: 262_144, ehandler: Rc::new(|e, _| e.into()), + content_type: None, } } } @@ -217,6 +232,7 @@ impl Default for JsonConfig { /// Returns error: /// /// * content type is not `application/json` +/// (unless specified in [`JsonConfig`](struct.JsonConfig.html)) /// * content length is greater than 256k /// /// # Server example @@ -253,10 +269,13 @@ pub struct JsonBody { impl JsonBody { /// Create `JsonBody` for request. - pub fn new(req: &T) -> Self { + pub fn new(req: &T, cfg: Option<&JsonConfig>) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) || + cfg.map_or(false, |cfg| { + cfg.content_type.as_ref().map_or(false, |predicate| predicate(mime)) + }) } else { false }; @@ -440,4 +459,61 @@ mod tests { .finish(); assert!(handler.handle(&req).as_err().is_none()) } + + #[test] + fn test_with_json_and_bad_content_type() { + let mut cfg = JsonConfig::default(); + cfg.limit(4096); + let handler = With::new(|data: Json| data, cfg); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain"), + ).header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + assert!(handler.handle(&req).as_err().is_some()) + } + + #[test] + fn test_with_json_and_good_custom_content_type() { + let mut cfg = JsonConfig::default(); + cfg.limit(4096); + cfg.content_type(|mime: mime::Mime| { + mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN + }); + let handler = With::new(|data: Json| data, cfg); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain"), + ).header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + assert!(handler.handle(&req).as_err().is_none()) + } + + #[test] + fn test_with_json_and_bad_custom_content_type() { + let mut cfg = JsonConfig::default(); + cfg.limit(4096); + cfg.content_type(|mime: mime::Mime| { + mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN + }); + let handler = With::new(|data: Json| data, cfg); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/html"), + ).header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + assert!(handler.handle(&req).as_err().is_some()) + } }