diff --git a/Cargo.toml b/Cargo.toml index 66fd5b856..579b74575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ regex = "0.2" slab = "0.4" sha1 = "0.2" url = "1.5" +multipart-async = { version = "0.*", features=["server"]} # tokio bytes = "0.4" diff --git a/src/httprequest.rs b/src/httprequest.rs index 576a00159..5b5410228 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -1,12 +1,18 @@ //! HTTP Request message related code. -use std::str; +use std::{io, str}; +use std::collections::HashMap; +use bytes::{Bytes, BytesMut}; +use futures::{Async, Future, Stream, Poll}; use url::form_urlencoded; +use multipart_async::server::BodyChunk; use http::{header, Method, Version, Uri, HeaderMap}; use {Cookie, CookieParseError}; use {HttpRange, HttpRangeParseError}; use error::ParseError; use recognizer::Params; +use multipart::Multipart; +use payload::{Payload, PayloadError}; #[derive(Debug)] @@ -52,15 +58,6 @@ impl HttpRequest { &self.headers } - // /// The remote socket address of this request - // /// - // /// This is an `Option`, because some underlying transports may not have - // /// a socket address, such as Unix Sockets. - // /// - // /// This field is not used for outgoing requests. - // #[inline] - // pub fn remote_addr(&self) -> Option { self.remote_addr } - /// The target path of this Request. #[inline] pub fn path(&self) -> &str { @@ -178,4 +175,144 @@ impl HttpRequest { Ok(Vec::new()) } } + + /// Return stream to process BODY as multipart. + /// + /// Content-type: multipart/form-data; + pub fn multipart(&self, payload: Payload) -> Result, Payload> { + const BOUNDARY: &'static str = "boundary="; + + if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if let Some(start) = content_type.find(BOUNDARY) { + let start = start + BOUNDARY.len(); + let end = content_type[start..].find(';') + .map_or(content_type.len(), |end| start + end); + let boundary = &content_type[start .. end]; + + return Ok(Multipart::with_body(Req{pl: payload}, boundary)) + } + } + } + Err(payload) + } + + /// Parse `application/x-www-form-urlencoded` encoded body. + /// Return `UrlEncoded` future. It resolves to a `HashMap`. + /// + /// Returns error: + /// + /// * content type is not `application/x-www-form-urlencoded` + /// * transfer encoding is `chunked`. + /// * content-length is greater than 256k + pub fn urlencoded(&self, payload: Payload) -> Result { + if let Ok(chunked) = self.chunked() { + if chunked { + return Err(payload) + } + } + + if let Some(len) = self.headers().get(header::CONTENT_LENGTH) { + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + if len > 262_144 { + return Err(payload) + } + } else { + return Err(payload) + } + } else { + return Err(payload) + } + } + + if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if content_type.to_lowercase() == "application/x-www-form-urlencoded" { + return Ok(UrlEncoded{pl: payload, body: BytesMut::new()}) + } + } + } + + Err(payload) + } +} + + +#[doc(hidden)] +pub struct Req { + pl: Payload, +} + +#[doc(hidden)] +pub struct Chunk(Bytes); + +impl BodyChunk for Chunk { + #[inline] + fn split_at(mut self, idx: usize) -> (Self, Self) { + (Chunk(self.0.split_to(idx)), self) + } + + #[inline] + fn as_slice(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Stream for Req { + type Item = Chunk; + type Error = io::Error; + + fn poll(&mut self) -> Poll, io::Error> { + match self.pl.poll() { + Err(_) => + Err(io::Error::new(io::ErrorKind::InvalidData, "incomplete")), + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => Ok(Async::Ready(None)), + Ok(Async::Ready(Some(item))) => match item { + Ok(bytes) => Ok(Async::Ready(Some(Chunk(bytes)))), + Err(err) => match err { + PayloadError::Incomplete => + Err(io::Error::new(io::ErrorKind::InvalidData, "incomplete")), + PayloadError::ParseError(err) => + Err(err.into()) + } + } + } + } +} + + +/// Future that resolves to a parsed urlencoded values. +pub struct UrlEncoded { + pl: Payload, + body: BytesMut, +} + +impl Future for UrlEncoded { + type Item = HashMap; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + loop { + return match self.pl.poll() { + Err(_) => unreachable!(), + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + let mut m = HashMap::new(); + for (k, v) in form_urlencoded::parse(&self.body) { + m.insert(k.into(), v.into()); + } + Ok(Async::Ready(m)) + }, + Ok(Async::Ready(Some(item))) => match item { + Ok(bytes) => { + self.body.extend(bytes); + continue + }, + Err(err) => Err(err), + } + } + } + } } diff --git a/src/lib.rs b/src/lib.rs index e1ebba87a..952d453be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ extern crate http; extern crate httparse; extern crate http_range; extern crate mime_guess; +extern crate multipart_async; extern crate url; extern crate actix; @@ -48,7 +49,7 @@ pub mod dev; pub mod httpcodes; pub use error::ParseError; pub use application::{Application, ApplicationBuilder}; -pub use httprequest::HttpRequest; +pub use httprequest::{HttpRequest, UrlEncoded}; pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder}; pub use payload::{Payload, PayloadItem, PayloadError}; pub use router::{Router, RoutingMap}; @@ -64,3 +65,9 @@ pub use http::{Method, StatusCode}; pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{ParseError as CookieParseError}; pub use http_range::{HttpRange, HttpRangeParseError}; + +/// Multipart support +pub mod multipart { + pub use multipart_async::server::{ + Field, FieldData, FieldHeaders, Multipart, ReadTextField, TextField}; +}