From 73dfd4dc260eafcde5252eab736470302bf1a11c Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Thu, 15 Nov 2018 16:06:25 +0100 Subject: [PATCH 1/6] Add form data extraction from body via serde_qs This makes it possible extract x-www-form-urlencoded data from the request body via serde_qs. This means that even nested fields can be extracted via the same `foo[bar]` syntax used in node's qs and Ruby on Rails. This is somewhat of an opinionated decision, but the web ecosystem seems to be converging on this and it is a huge boon when submitting complex form data. The alternative is only allowing a flat map of key->values but this is very limiting. Another decision here is the naming of the extractor. Since there are two different form encodings, `application/x-www-form-urlencoded` and `multipart/form-data`* it might seem reasonable to name this extractor `body::UrlencodedForm` or something, but I think that choosing the simpler `body::Form` name is the better choice here. The reason being that urlencoded forms are far more uncommon than multipart, and urlencoded is the standard when no encoding is specified. In other words, multipart forms really are a special case, and having both `Form` and `MultipartForm` would make the distinction clear. actix-web makes the same choice here, opting for `Form` and `Multipart` as names. Closes #22 * well technically there is also `text/plain`, but it seems to be mostly useless --- Cargo.toml | 1 + examples/body_types.rs | 7 +++++++ src/body.rs | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 78cceea4d..de743fdb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde = "1.0.80" serde_derive = "1.0.80" serde_json = "1.0.32" typemap = "0.3.3" +serde_qs = "0.4.1" [dependencies.futures-preview] features = ["compat"] diff --git a/examples/body_types.rs b/examples/body_types.rs index 663af301c..185ec2c5b 100644 --- a/examples/body_types.rs +++ b/examples/body_types.rs @@ -32,11 +32,18 @@ async fn echo_json(msg: body::Json) -> body::Json { msg } +async fn echo_form(msg: body::Form) -> body::Form { + println!("Form: {:?}", msg.0); + + msg +} + fn main() { let mut app = tide::App::new(()); app.at("/echo/string").post(echo_string); app.at("/echo/string_lossy").post(echo_string_lossy); app.at("/echo/vec").post(echo_vec); app.at("/echo/json").post(echo_json); + app.at("/echo/form").post(echo_form); app.serve("127.0.0.1:8000"); } diff --git a/src/body.rs b/src/body.rs index 39284c17e..38d588bac 100644 --- a/src/body.rs +++ b/src/body.rs @@ -150,6 +150,42 @@ impl IntoResponse for Json { } } +/// A wrapper for form encoded (application/x-www-form-urlencoded) (de)serialization of bodies. +/// +/// This type is usable both as an extractor (argument to an endpoint) and as a response +/// (return value from an endpoint), though returning a response with form data is uncommon +/// and probably not good practice. +pub struct Form(pub T); + +impl Extract for Form { + // Note: cannot use `existential type` here due to ICE + type Fut = FutureObj<'static, Result>; + + fn extract(data: &mut S, req: &mut Request, params: &RouteMatch<'_>) -> Self::Fut { + let mut body = std::mem::replace(req.body_mut(), Body::empty()); + FutureObj::new(Box::new( + async move { + let body = await!(body.read_to_vec()).map_err(mk_err)?; + let data: T = serde_qs::from_bytes(&body).map_err(mk_err)?; + Ok(Form(data)) + }, + )) + } +} + +impl IntoResponse for Form { + fn into_response(self) -> Response { + // TODO: think about how to handle errors + http::Response::builder() + .status(http::status::StatusCode::OK) + .header("Content-Type", "Application/x-www-form-urlencoded") + .body(Body::from( + serde_qs::to_string(&self.0).unwrap().into_bytes(), + )) + .unwrap() + } +} + pub struct Str(pub String); impl Extract for Str { From 8fe1daa530c6146b408a7102760493a4001a3e8f Mon Sep 17 00:00:00 2001 From: Jonas Nicklas Date: Tue, 20 Nov 2018 14:03:15 +0100 Subject: [PATCH 2/6] Downcase content types --- src/body.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/body.rs b/src/body.rs index 38d588bac..58d059c2a 100644 --- a/src/body.rs +++ b/src/body.rs @@ -144,7 +144,7 @@ impl IntoResponse for Json { // TODO: think about how to handle errors http::Response::builder() .status(http::status::StatusCode::OK) - .header("Content-Type", "Application/json") + .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&self.0).unwrap())) .unwrap() } @@ -178,7 +178,7 @@ impl IntoResponse for Form { // TODO: think about how to handle errors http::Response::builder() .status(http::status::StatusCode::OK) - .header("Content-Type", "Application/x-www-form-urlencoded") + .header("Content-Type", "application/x-www-form-urlencoded") .body(Body::from( serde_qs::to_string(&self.0).unwrap().into_bytes(), )) From e45f6a592c66697c70f96fc58beac9b08f72bcc9 Mon Sep 17 00:00:00 2001 From: lixiaohui Date: Wed, 21 Nov 2018 22:15:32 +0800 Subject: [PATCH 3/6] add rest method to Resource --- src/router.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/router.rs b/src/router.rs index 5f075f339..469fbdef2 100644 --- a/src/router.rs +++ b/src/router.rs @@ -86,4 +86,24 @@ impl Resource { pub fn delete, U>(&mut self, ep: T) { self.method(http::Method::DELETE, ep) } + + /// Add an endpoint for `OPTIONS` requests + pub fn options, U>(&mut self, ep: T) { + self.method(http::Method::OPTIONS, ep) + } + + /// Add an endpoint for `CONNECT` requests + pub fn connect, U>(&mut self, ep: T) { + self.method(http::Method::CONNECT, ep) + } + + /// Add an endpoint for `PATCH` requests + pub fn patch, U>(&mut self, ep: T) { + self.method(http::Method::PATCH, ep) + } + + /// Add an endpoint for `TRACE` requests + pub fn trace, U>(&mut self, ep: T) { + self.method(http::Method::TRACE, ep) + } } From cb11f3a80b0952e8128a849289f29a206fbd1097 Mon Sep 17 00:00:00 2001 From: Fuyang Liu Date: Sun, 18 Nov 2018 21:19:18 +0100 Subject: [PATCH 4/6] Allow handel file upload via multipart form --- Cargo.toml | 1 + examples/multipart-form/main.rs | 74 ++++++++++++++++++++++++++++++++ examples/multipart-form/test.txt | 1 + src/body.rs | 35 +++++++++++++++ src/head.rs | 5 +++ 5 files changed, 116 insertions(+) create mode 100644 examples/multipart-form/main.rs create mode 100644 examples/multipart-form/test.txt diff --git a/Cargo.toml b/Cargo.toml index de743fdb8..f855edc3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde_derive = "1.0.80" serde_json = "1.0.32" typemap = "0.3.3" serde_qs = "0.4.1" +multipart = "0.15.3" [dependencies.futures-preview] features = ["compat"] diff --git a/examples/multipart-form/main.rs b/examples/multipart-form/main.rs new file mode 100644 index 000000000..9770f9ec2 --- /dev/null +++ b/examples/multipart-form/main.rs @@ -0,0 +1,74 @@ +#![feature(async_await, futures_api)] + +#[macro_use] +extern crate serde_derive; + +use http::status::StatusCode; +use std::io::Read; +use tide::{body, App}; + +#[derive(Serialize, Deserialize, Clone)] +struct Message { + key1: Option, + key2: Option, + file: Option, +} + +async fn upload_file( + multipart_form: body::MultipartForm, +) -> Result, StatusCode> { + // https://stackoverflow.com/questions/43424982/how-to-parse-multipart-forms-using-abonander-multipart-with-rocket + let mut message = Message { + key1: None, + key2: None, + file: None, + }; + + let mut multipart = multipart_form.0; + multipart + .foreach_entry(|mut entry| match entry.headers.name.as_str() { + "file" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + message.file = String::from_utf8(vec).ok(); + println!("key file got"); + } + + "key1" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + message.key1 = String::from_utf8(vec).ok(); + println!("key1 got"); + } + + "key2" => { + let mut vec = Vec::new(); + entry.data.read_to_end(&mut vec).expect("can't read"); + message.key2 = String::from_utf8(vec).ok(); + println!("key2 got"); + } + + _ => { + // as multipart has a bug https://github.com/abonander/multipart/issues/114 + // we manually do read_to_end here + let mut _vec = Vec::new(); + entry.data.read_to_end(&mut _vec).expect("can't read"); + println!("key neglected"); + } + }) + .expect("Unable to iterate multipart?"); + + Ok(body::Json(message)) +} + +fn main() { + let mut app = App::new(()); + + app.at("/upload_file").post(upload_file); + + app.serve("127.0.0.1:7878"); +} + +// Test with: +// curl -X POST http://localhost:7878/upload_file -H 'content-type: multipart/form-data' -F file=@examples/multipart-form/test.txt +// curl -X POST http://localhost:7878/upload_file -H 'content-type: multipart/form-data' -F key1=v1, -F key2=v2 diff --git a/examples/multipart-form/test.txt b/examples/multipart-form/test.txt new file mode 100644 index 000000000..af27ff498 --- /dev/null +++ b/examples/multipart-form/test.txt @@ -0,0 +1 @@ +This is a test file. \ No newline at end of file diff --git a/src/body.rs b/src/body.rs index 58d059c2a..ffb097da2 100644 --- a/src/body.rs +++ b/src/body.rs @@ -5,7 +5,9 @@ use futures::{compat::Compat01As03, future::FutureObj, prelude::*, stream::StreamObj}; use http::status::StatusCode; +use multipart::server::Multipart; use pin_utils::pin_mut; +use std::io::Cursor; use crate::{Extract, IntoResponse, Request, Response, RouteMatch}; @@ -117,6 +119,39 @@ fn mk_err(_: T) -> Response { StatusCode::BAD_REQUEST.into_response() } +/// A wrapper for multipart form +/// +/// This type is useable as an extractor (argument to an endpoint) for getting +/// a Multipart type defined in the multipart crate +pub struct MultipartForm(pub Multipart>>); + +impl Extract for MultipartForm { + // Note: cannot use `existential type` here due to ICE + type Fut = FutureObj<'static, Result>; + + fn extract(data: &mut S, req: &mut Request, params: &RouteMatch<'_>) -> Self::Fut { + // https://stackoverflow.com/questions/43424982/how-to-parse-multipart-forms-using-abonander-multipart-with-rocket + + const BOUNDARY: &str = "boundary="; + let boundary = req.headers().get("content-type").and_then(|ct| { + let ct = ct.to_str().ok()?; + let idx = ct.find(BOUNDARY)?; + Some(ct[idx + BOUNDARY.len()..].to_string()) + }); + + let mut body = std::mem::replace(req.body_mut(), Body::empty()); + + FutureObj::new(Box::new( + async move { + let body = await!(body.read_to_vec()).map_err(mk_err)?; + let boundary = boundary.ok_or(()).map_err(mk_err)?; + let mp = Multipart::with_body(Cursor::new(body), boundary); + Ok(MultipartForm(mp)) + }, + )) + } +} + /// A wrapper for json (de)serialization of bodies. /// /// This type is usable both as an extractor (argument to an endpoint) and as a response diff --git a/src/head.rs b/src/head.rs index 262df78f3..9cbe2279b 100644 --- a/src/head.rs +++ b/src/head.rs @@ -44,6 +44,11 @@ impl Head { pub fn method(&self) -> &http::Method { &self.inner.method } + + /// The HTTP headers + pub fn headers(&self) -> &http::header::HeaderMap { + &self.inner.headers + } } /// An extractor for path components. From 2215e2d7232484d05ac02d69435a7af6a61a8dc4 Mon Sep 17 00:00:00 2001 From: Wonwoo Choi Date: Sat, 24 Nov 2018 20:26:58 +0900 Subject: [PATCH 5/6] Relieve middleware FutureObj lifetime bounds --- src/app.rs | 7 +++---- src/middleware/default_headers.rs | 25 ++++++++++++++----------- src/middleware/mod.rs | 28 ++++++++++++++-------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2a14d95c2..c67276074 100644 --- a/src/app.rs +++ b/src/app.rs @@ -106,16 +106,15 @@ impl Service for Server { async move { if let Some((endpoint, params)) = router.route(&path, &method) { for m in middleware.iter() { - match await!(m.request(&mut data, req, ¶ms)) { - Ok(new_req) => req = new_req, - Err(resp) => return Ok(resp.map(Into::into)), + if let Some(resp) = await!(m.request(&mut data, &mut req, ¶ms)) { + return Ok(resp.map(Into::into)); } } let (head, mut resp) = await!(endpoint.call(data.clone(), req, params)); for m in middleware.iter() { - resp = await!(m.response(&mut data, &head, resp)) + await!(m.response(&mut data, &head, &mut resp)); } Ok(resp.map(Into::into)) diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index 94d15edfb..a792cbcdb 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -34,16 +34,19 @@ impl DefaultHeaders { } impl Middleware for DefaultHeaders { - fn response( - &self, - data: &mut Data, - head: &Head, - mut resp: Response, - ) -> FutureObj<'static, Response> { - let headers = resp.headers_mut(); - for (key, value) in self.headers.iter() { - headers.entry(key).unwrap().or_insert_with(|| value.clone()); - } - FutureObj::new(Box::new(async { resp })) + fn response<'a>( + &'a self, + data: &'a mut Data, + head: &'a Head, + resp: &'a mut Response, + ) -> FutureObj<'a, ()> { + FutureObj::new(Box::new( + async move { + let headers = resp.headers_mut(); + for (key, value) in self.headers.iter() { + headers.entry(key).unwrap().or_insert_with(|| value.clone()); + } + }, + )) } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index b11fb0235..5350f3ce1 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -10,23 +10,23 @@ pub use self::default_headers::DefaultHeaders; pub trait Middleware: Send + Sync { /// Asynchronously transform the incoming request, or abort further handling by immediately /// returning a response. - fn request( - &self, - data: &mut Data, - req: Request, - params: &RouteMatch<'_>, - ) -> FutureObj<'static, Result> { - FutureObj::new(Box::new(async { Ok(req) })) + fn request<'a>( + &'a self, + data: &'a mut Data, + req: &'a mut Request, + params: &'a RouteMatch<'_>, + ) -> FutureObj<'a, Option> { + FutureObj::new(Box::new(async { None })) } /// Asynchronously transform the outgoing response. - fn response( - &self, - data: &mut Data, - head: &Head, - resp: Response, - ) -> FutureObj<'static, Response> { - FutureObj::new(Box::new(async { resp })) + fn response<'a>( + &'a self, + data: &'a mut Data, + head: &'a Head, + resp: &'a mut Response, + ) -> FutureObj<'a, ()> { + FutureObj::new(Box::new(async {})) } // TODO: provide the following, intended to fire *after* the body has been fully sent From c046ce6f70e1a3036497d85b91cdca22b4cf0fb0 Mon Sep 17 00:00:00 2001 From: Wonwoo Choi Date: Sat, 24 Nov 2018 22:54:37 +0900 Subject: [PATCH 6/6] Change return type of `request` to Result<(), Response> --- src/app.rs | 2 +- src/middleware/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index c67276074..0ebfcab7f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -106,7 +106,7 @@ impl Service for Server { async move { if let Some((endpoint, params)) = router.route(&path, &method) { for m in middleware.iter() { - if let Some(resp) = await!(m.request(&mut data, &mut req, ¶ms)) { + if let Err(resp) = await!(m.request(&mut data, &mut req, ¶ms)) { return Ok(resp.map(Into::into)); } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 5350f3ce1..4291199e8 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -15,8 +15,8 @@ pub trait Middleware: Send + Sync { data: &'a mut Data, req: &'a mut Request, params: &'a RouteMatch<'_>, - ) -> FutureObj<'a, Option> { - FutureObj::new(Box::new(async { None })) + ) -> FutureObj<'a, Result<(), Response>> { + FutureObj::new(Box::new(async { Ok(()) })) } /// Asynchronously transform the outgoing response.